<?php
namespace Paradox\toolbox;
use Paradox\exceptions\PodManagerException;
use Paradox\Toolbox;
use Paradox\pod\Document;
use Paradox\pod\Vertex;
use Paradox\pod\Edge;
use Paradox\AModel;
use Paradox\AObservable;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Pod manager
 * Manages the lifecycle (dispense, load data, save, delete) of pods.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class PodManager extends AObservable
{
    /**
     * A reference to the toolbox.
     * @var Toolbox
     */
    private $_toolbox;

    /**
     * Instantiates the pod manager.
     * @param Toolbox $toolbox
     */
    public function __construct(Toolbox $toolbox)
    {
        $this->_toolbox = $toolbox;
    }

    /**
     * Sets up a pod enclosed in a model.
     * @param  string $collection The type of pod you want to dispense. For graphs, only "vertex" and "edge" is allowed. For documents, the name of the collection.
     * @param  string $label      If you are dispensing an edge, you can optionally provide a label for that edge.
     * @return AModel
     */
    public function dispense($type, $label = null)
    {
        if ($label !== null && (!$this->_toolbox->isGraph() || ($this->_toolbox->isGraph() && strtolower($type) != "edge"))) {
            throw new PodManagerException("Only Edge pods can have a label passed into dispense().");
        }

        //Graph pods
        if ($this->_toolbox->isGraph()) {

            switch (strtolower($type)) {

                case "vertex":
                    $model = $this->createVertex();
                    break;

                case "edge":
                    $model = $this->createEdge();

                    if ($label) {
                        $model->set('$label', $label);
                    }

                    break;

                default:
                    throw new PodManagerException("When dispensing pods for graphs, only the types 'vertex' and 'edge' are allowed. You provided '$type'.");
            }

        //Document pods
        } else {
            $model = $this->createDocument($type);
        }

        //Signal here
        $this->notify("after_dispense", $model->getPod());

        return $model;
    }

    /**
     * Persists a model to the server.
     * @param  AModel $model
     * @return int    The id of the saved pod.
     */
    public function store(AModel $model)
    {
        $driver = $this->_toolbox->getDriver();
        $pod = $model->getPod();

        //Signal here
        $this->notify("before_store", $pod);

        $id = null;

        try {
            switch ($pod) {

                   case $pod instanceof Vertex:
                       if ($pod->isNew()) {

                           if ($this->hasTransaction()) {
                               $id = $this->determinePreviouslyStored($model);
                               $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getVertexCollectionName());

                               //In a transaction, since all commands are executed in a block on commit, the pod will not have a saved state if it has a previous
                               //store command. This determines if that is the case and rewrites the command as a replace.
                               if ($id !== false) {
                                       return $this->addTransactionCommand("db.{$this->_toolbox->getVertexCollectionName()}.replace(result.$id._id, {$pod->toTransactionJSON()});", "PodManager:store", $model, true);
                               } else {
                                       return $this->addTransactionCommand("graph.addVertex(null, {$pod->toTransactionJSON()})._properties;", "PodManager:store", $model, true);
                               }

                           } else {
                               $doc = $pod->toDriverDocument();
                               $driver->saveVertex($this->_toolbox->getGraph(), $doc);
                               $id = $doc->getInternalId();
                           }

                       } else {

                           if ($this->hasTransaction()) {
                               $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getVertexCollectionName());

                               return $this->addTransactionCommand("db.{$this->_toolbox->getVertexCollectionName()}.replace('{$pod->getId()}', {$pod->toTransactionJSON()});", "PodManager:store", $model, true);
                           } else {
                               $doc = $pod->toDriverDocument();
                               $driver->replaceVertex($this->_toolbox->getGraph(), $pod->getId(), $doc);
                           }
                    }
                    break;

                   case $pod instanceof Edge:

                       //Check to see if we have existing keys for the _to and _from vertices
                       $fromKey = $pod->getFromId();
                       $toKey = $pod->getToId();
                       $fromKeyIsJSVar = false;
                       $toKeyIsJSVar = false;

                       //If we don't have a key for from
                       if (!$fromKey) {

                           //Get the from vertex
                        $from = $pod->getFrom();

                        //Save the vertices to get a key for them
                           if ($from) {
                               if ($from->hasChanged()) {

                                   if ($this->hasTransaction()) {
                                       $fromKey = $this->store($from);
                                       $fromKeyIsJSVar = true;
                                   } else {
                                       $this->store($from);
                                       $fromKey = $from->getPod()->getId();
                                       $pod->setFrom($from);
                                   }
                               }

                               //If there are no vertices, throw an error
                        } else {
                            throw new PodManagerException("An edge must have a valid 'from' vertex.");
                        }
                    }

                       //If we don't have a key for to
                       if (!$toKey) {

                        //Get the from vertex
                        $to = $pod->getTo();

                        //Save the vertices to get a key for them
                           if ($to) {
                               if ($to->hasChanged()) {

                                   if ($this->hasTransaction()) {
                                       $toKey = $this->store($to);
                                       $toKeyIsJSVar = true;
                                   } else {
                                       $this->store($to);
                                       $toKey = $to->getPod()->getId();
                                       $pod->setTo($to);
                                   }
                               }

                        //If there are no vertices, throw an error
                        } else {
                            throw new PodManagerException("An edge must have a valid 'to' vertex.");
                        }
                       }

                       if ($pod->isNew()) {

                           if ($this->hasTransaction()) {

                               $id = $this->determinePreviouslyStored($model);
                               $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getEdgeCollectionName());

                               //In a transaction, since all commands are executed in a block on commit, the pod will not have a saved state if it has a previous
                               //store command. This determines if that is the case and rewrites the command as a replace.
                               if ($id !== false) {
                                       $this->addTransactionCommand("function () {graph.removeEdge(graph.getEdge(result.$id._id)); return true;}();", "PodManager:store", $model, true);
                                       $this->addTransactionCommand($this->generateCreateEdgeCommand($fromKeyIsJSVar, $fromKey, $toKeyIsJSVar, $toKey, $pod->toTransactionJSON(), "result.$id._key", true, $pod->getLabel()), "PodManager:store", $model, true);
                               } else {
                                       $this->addTransactionCommand($this->generateCreateEdgeCommand($fromKeyIsJSVar, $fromKey, $toKeyIsJSVar, $toKey, $pod->toTransactionJSON(), null, false, $pod->getLabel()), "PodManager:store", $model, true);
                               }
                           } else {
                               $doc = $pod->toDriverDocument();

                               $driver->saveEdge($this->_toolbox->getGraph(), $fromKey, $toKey, null, $doc);
                               $id = $doc->getInternalId();
                           }

                       } else {
                        //We delete the pod then save it, because ArangoDB does not provide a way to update the To and From vertices.
                        if ($this->hasTransaction()) {
                            $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getEdgeCollectionName());
                            $this->addTransactionCommand("function () {graph.removeEdge(graph.getEdge('{$pod->getId()}')); return true;}();", "PodManager:store", $model, true);
                            $this->addTransactionCommand($this->generateCreateEdgeCommand($fromKeyIsJSVar, $fromKey, $toKeyIsJSVar, $toKey, $pod->toTransactionJSON(), $pod->getKey(), false, $pod->getLabel()), "PodManager:store", $model, true);
                        } else {
                            $id = $pod->getId();
                            $revision = $pod->getRevision() + mt_rand(0, 1000);
                            $this->delete($model);

                            //Add the id and revision back, because deleting removes it
                            $pod->setId($id);
                            $pod->setRevision($revision);

                            $doc = $pod->toDriverDocument();
                            $driver->saveEdge($this->_toolbox->getGraph(), $fromKey, $toKey, null, $doc);
                        }
                    }
                    break;

                default:
                    $doc = $pod->toDriverDocument();

                    if ($pod->isNew()) {

                        if ($this->hasTransaction()) {
                            $id = $this->determinePreviouslyStored($model);
                            $this->_toolbox->getTransactionManager()->addWriteCollection($pod->getType());

                            //In a transaction, since all commands are executed in a block on commit, the pod will not have a saved state if it has a previous
                            //store command. This determines if that is the case and rewrites the command as a replace.
                            if ($id !== false) {
                                $this->addTransactionCommand("db.{$pod->getType()}.replace(result.$id._id, {$pod->toTransactionJSON()}, true);", "PodManager:store", $model);
                            } else {
                                $this->addTransactionCommand("db.{$pod->getType()}.save({$pod->toTransactionJSON()});", "PodManager:store", $model);
                            }

                        } else {
                            $driver->save($pod->getType(), $doc);
                            $id = $doc->getInternalId();
                        }

                    } else {

                        if ($this->hasTransaction()) {
                            $this->_toolbox->getTransactionManager()->addWriteCollection($pod->getType());
                            $this->addTransactionCommand("db.{$pod->getType()}.replace('{$pod->getId()}', {$pod->toTransactionJSON()}, true);", "PodManager:store", $model);
                        } else {
                            $driver->replace($doc);
                        }
                    }

            }
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new PodManagerException($normalised['message'], $normalised['code']);
        }

        if (!$this->hasTransaction()) {
            return $this->processStoreResult($pod, $doc->getRevision(), $id);
        }
    }

    /**
     * Deletes a model from the server.
     * @param  AModel  $model
     * @return boolean
     */
    public function delete(AModel $model)
    {
        $driver = $this->_toolbox->getDriver();
        $pod = $model->getPod();

        //Signal here
        $this->notify("before_delete", $pod);

        try {
            if ($pod instanceof Vertex) {

                if ($this->hasTransaction()) {
                    $id = $this->determinePreviouslyStored($model);
                    $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getVertexCollectionName());

                    if ($id !== false) {
                        $this->addTransactionCommand("function () {graph.removeVertex(graph.getVertex(result.$id._id)); return true;}();", "PodManager:delete", $model, true);
                    } else {
                        $this->addTransactionCommand("function () {graph.removeVertex(graph.getVertex('{$pod->getId()}')); return true;}();", "PodManager:delete", $model, true);
                    }

                } else {
                    $driver->removeVertex($this->_toolbox->getGraph(), $pod->getId());
                }

            } elseif ($pod instanceof Edge) {

                if ($this->hasTransaction()) {
                    $id = $this->determinePreviouslyStored($model);
                    $this->_toolbox->getTransactionManager()->addWriteCollection($this->_toolbox->getEdgeCollectionName());

                    if ($id !== false) {
                        $this->addTransactionCommand("function () {graph.removeEdge(graph.getEdge(result.$id._id)); return true;}();", "PodManager:delete", $model, true);
                    } else {
                        $this->addTransactionCommand("function () {graph.removeEdge(graph.getEdge('{$pod->getId()}')); return true;}();", "PodManager:delete", $model, true);
                    }

                } else {
                    $driver->removeEdge($this->_toolbox->getGraph(), $pod->getId());
                }

            } else {

                if ($this->hasTransaction()) {

                    $id = $this->determinePreviouslyStored($model);
                    $this->_toolbox->getTransactionManager()->addWriteCollection($pod->getType());

                    if ($id !== false) {
                        $this->addTransactionCommand("function () {db.{$pod->getType()}.remove(result.$id._id, true); return true;}();", "PodManager:delete", $model);
                    } else {
                        $this->addTransactionCommand("function () {db.{$pod->getType()}.remove('{$pod->getId()}', true); return true;}();", "PodManager:delete", $model);
                    }

                } else {
                    $driver->delete($pod->toDriverDocument());
                }

            }
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new PodManagerException($normalised['message'], $normalised['code']);
        }

        if (!$this->hasTransaction()) {
            return $this->processDeleteResult($pod);
        }

    }

    /**
     * Loads a model from the server using the collection type and the id. Returns null if the pod could not be found or loaded.
     * @param  string $collection The collection to load from. For graphs, this can only be "vertex" or "edge".
     * @param  string $id         The id (key in ArangoDB jargon) of the document, edge or vertex.
     * @return AModel
     */
    public function load($type, $id)
    {
        $driver = $this->_toolbox->getDriver();

        try {
            if ($this->_toolbox->isGraph()) {

                switch (strtolower($type)) {

                    case "vertex":

                        if ($this->hasTransaction()) {
                            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getVertexCollectionName());
                            $this->addTransactionCommand("function () {var temp = graph.getVertex('$id'); return temp ? temp._properties : null}();", "PodManager:load", null, true, array('type' => $type));
                        } else {
                            $vertex = $driver->getVertex($this->_toolbox->getGraph(), $id);

                            return $this->convertDriverDocumentToPod($vertex);
                        }
                        break;

                    case "edge":

                        if ($this->hasTransaction()) {
                            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getEdgeCollectionName());
                            $this->addTransactionCommand("graph.getEdge('$id')._properties;", "PodManager:load", null, true, array('type' => $type));
                        } else {
                            $edge = $driver->getEdge($this->_toolbox->getGraph(), $id);

                            return $this->convertDriverDocumentToPod($edge);
                        }
                        break;

                    default:
                        throw new PodManagerException("For graphs, only the types 'vertex' and 'edge' can be loaded.");
                }

            } else {

                if ($this->hasTransaction()) {
                    $this->_toolbox->getTransactionManager()->addReadCollection($type);
                    $this->addTransactionCommand("db.$type.document('$id');", "PodManager:load", null, false, array('type' => $type));
                } else {
                    $document = $driver->getById($type, $id);

                    return $this->convertDriverDocumentToPod($document);
                }

            }
        } catch (\Exception $e) {

            //Rethrow the exception from the try block
            if ($e instanceof PodManagerException) {
                throw $e;

            //Otherwise just return a null if the pod does not exist, or if there is an error.
            } else {
                return null;
            }
        }

    }

    public function processDeleteResult($pod)
    {
        //Signal here
        $pod->resetMeta();
        $this->notify("after_delete", $pod);

        return true;
    }

    /**
     * Process the result after storing a pod.
     * @param Paraodx\pod\Document|Paradox\pod\Vertex|Paradox\pod\Edge $pod      The pod to process.
     * @param string                                                   $revision The revision of the pod.
     * @param string                                                   $id       The id of the pod.
     */
    public function processStoreResult($pod, $revision, $id = null)
    {
        $pod->setSaved();
        $pod->setRevision($revision);

        if ($id && $pod->getId() === null) {
            $pod->setId($id);
        }

        //Signal here
        $this->notify("after_store", $pod);

        return $pod->getKey();
    }

    /**
     * Converts an array of documents into pods. The documents in each array can be an ArangoDB-PHP document, edge or vertex or an associative array.
     * @param  string $type      The type the resulting pods should have.
     * @param  array  $documents An array of documents or associative arrays.
     * @return array
     */
    public function convertToPods($type, array $documents)
    {
        $result = array();

        foreach ($documents as $document) {

            if ($document instanceof \triagens\ArangoDb\Document) {
                $model = $this->convertDriverDocumentToPod($document);

                $result[$document->getInternalId()] = $model;

            } else {
                $model = $this->convertArrayToPod($type, $document);

                if (isset($document['_id'])) {
                    $result[$document['_id']] = $model;
                } else {
                    $result[] = $model;
                }
            }
        }

        return $result;
    }

    /**
     * Converts an associative array to a pod.
     * @param  string $type The type of the resulting pod.
     * @param  array  $data An associative array containing the data for the pod.
     * @return AModel
     */
    public function convertArrayToPod($type, array $data)
    {
        $model = $this->dispense($type);
        $pod = $model->getPod();

        $pod->loadFromArray($data);

        //Signal here
        $this->notify("after_open", $model->getPod());

        return $model;
    }

    /**
     * Converts an ArangoDB-PHP document, edge or vertex into a pod.
     * @param  \triagens\ArangoDb\Document $driverDocument The ArangoDB-PHP document, edge or vertex.
     * @return AModel
     */
    public function convertDriverDocumentToPod(\triagens\ArangoDb\Document $driverDocument)
    {
        switch ($driverDocument) {
            case $driverDocument instanceof \triagens\ArangoDb\Vertex:
                $model = $this->dispense("vertex");
                break;

            case $driverDocument instanceof \triagens\ArangoDb\Edge:
                $model = $this->dispense("edge");
                break;

            default:
                @list($collection,) = explode('/', $driverDocument->getInternalId(), 2);
                $model = $this->dispense($collection);
        }

        $model->getPod()->loadFromDriver($driverDocument);
        $model->getPod()->setSaved();

        //Signal here
        $this->notify("after_open", $model->getPod());

        return $model;
    }

    /**
     * Generates the javascript command to create an edge (for use with transactions).
     * @param  boolean $fromKeyIsJSVar Whether the from key is a javascript variable name or not.
     * @param  string  $fromKey        The id of the from vertex.
     * @param  boolean $toKeyIsJSVar   Whether the to key is a javascript variable name or not.
     * @param  string  $toKey          The id of the to vertex.
     * @param  string  $data           JSON representation of the edge data.
     * @param  string  $id             An optional id for the edge.
     * @param  boolean $idIsVariable   Whether the id string is a javascript variable.
     * @param  string  $label          The optional label of the edge
     * @return string
     */
    private function generateCreateEdgeCommand($fromKeyIsJSVar, $fromKey, $toKeyIsJSVar, $toKey, $data, $id = null, $idIsVariable = false, $label = null)
    {
        $command = "graph.addEdge(";

        if ($fromKeyIsJSVar) {
            $command .= "graph.getVertex(result.$fromKey._id)";
        } else {
            $command .= "graph.getVertex('$fromKey')";
        }

        $command .= ", ";

        if ($toKeyIsJSVar) {
            $command .= "graph.getVertex(result.$toKey._id)";
        } else {
            $command .= "graph.getVertex('$toKey')";
        }

        $command .= ", ";

        if ($id) {

            if ($idIsVariable) {
                $command .= "$id";
            } else {
                $command .= "'$id'";
            }

        } else {
            $command .= "null";
        }

        if ($label) {
            $command .= ", '$label'";
        }

        $command .= ", $data)._properties;";

        return $command;
    }

    /**
     * Convinence function to instantiate a vertex, hook up its events and set up its model.
     * @param array  $data An optional array of data that will be loaded into the vertex.
     * @param string $new  Whether this pod should be marked as new (never saved to the server).
     */
    private function createVertex(array $data = array(), $new = true)
    {
        $vertex = new Vertex($this->_toolbox, $data, $new);
        $this->attachEventsToPod($vertex);

        return $this->setupModel($vertex, $vertex);
    }

    /**
     * Convinence function to instantiate an edge, hook up its events and set up its model.
     * @param array  $data An optional array of data that will be loaded into the edge.
     * @param string $new  Whether this pod should be marked as new (never saved to the server).
     */
    private function createEdge(array $data = array(), $new = true)
    {
        $edge = new Edge($this->_toolbox, $data, $new);
        $this->attachEventsToPod($edge);

        return $this->setupModel($edge, $edge);
    }

    /**
     * Convinence function to instantiate a document, hook up its events and set up its model.
     * @param array  $data An optional array of data that will be loaded into the document.
     * @param string $new  Whether this pod should be marked as new (never saved to the server).
     */
    private function createDocument($type, $data = array(), $new = true)
    {
        $document = new Document($this->_toolbox, $type, $data, $new);
        $this->attachEventsToPod($document);

        return $this->setupModel($document, $document);
    }

    /**
     * Convience function that hooks up events emmited by this pod manager to the pod.
     * @param Document $pod The pod we want to hook up events to.
     */
    private function attachEventsToPod(Document $pod)
    {
        $this->attach(array('after_dispense', 'after_open', 'before_store', 'after_store', 'before_delete', 'after_delete'), $pod);
    }

    /**
     * Convinence function that sets up the model for a pod and associates the model with it.
     * @param  Document            $pod The pod we want to set up the model for.
     * @throws PodManagerException
     * @return AModel
     */
    private function setupModel(Document $pod)
    {
        $name = $this->_toolbox->formatModel($pod);

        $model = new $name();

        if (!($model instanceof AModel)) {
            throw new PodManagerException("Custom models must inherit from the Paradox Model class.");
        }

        $model->loadPod($pod);

        $pod->loadModel($model);

        return $model;
    }

    /**
     * Because transactions are executed in 1 go, this helps us deteremine if the pod has a previous command to store it.
     * We can then use its id to work with it in the transaction.
     * @param  AModel         $model The model to determine whether to delete or not.
     * @return boolean|string
     */
    private function determinePreviouslyStored($model)
    {
        $store = $this->_toolbox->getTransactionManager()->searchCommandsByActionAndObject('PodManager:store', $model);
        $delete = $this->_toolbox->getTransactionManager()->searchCommandsByActionAndObject('PodManager:delete', $model);

        $storePosition = $store ? $store['position'] : -1;
        $deletePosition = $delete ? $delete['position'] : -1;

        if ($deletePosition >= $storePosition) {
            return false;
        } else {
            return $store['id'];
        }
    }

    /**
     * Whether the connection current has an active transaction.
     * @return boolean
     */
    private function hasTransaction()
    {
        return $this->_toolbox->getTransactionManager()->hasTransaction();
    }

    /**
     * Convinence function to add commands to the transaction.
     * @param unknown $command
     * @param unknown $action
     * @param string  $object
     * @param string  $isGraph
     */
    private function addTransactionCommand($command, $action, $object = null, $isGraph = false, $data = array())
    {
        return $this->_toolbox->getTransactionManager()->addCommand($command, $action, $object, $isGraph, $data);
    }

    /**
     * Validates the type to make sure it is valid in the current context. For graphs, a type other than "edge" or "vertex" would be invalid.
     * @param  string  $type The type to validate.
     * @return boolean
     */
    public function validateType($type)
    {
        if ($this->_toolbox->isGraph()) {

            if (!in_array(strtolower($type), array('edge', 'vertex'))) {
                return false;
            }
        }

        return true;
    }
}


<?php

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Util;

final class ApiHelper
{
    /**
     * Cleans the api response, so that you always get a list of elements.
     *
     * @param callable $callback
     * @param array    $data
     *
     * @return array
     */
    public static function mapList(callable $callback, array $data): array
    {
        if (!isset($data[0])) {
            $data = [$data];
        }

        return array_map($callback, $data);
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Service;

use Core23\LastFm\Client\ApiClientInterface;
use Core23\LastFm\Model\ArtistInfo;
use Core23\LastFm\Util\ApiHelper;

final class LibraryService implements LibraryServiceInterface
{
    /**
     * @var ApiClientInterface
     */
    private $client;

    /**
     * @param ApiClientInterface $client
     */
    public function __construct(ApiClientInterface $client)
    {
        $this->client = $client;
    }

    /**
     * {@inheritdoc}
     */
    public function getArtists(string $user, int $limit = 50, int $page = 1): array
    {
        $response = $this->client->unsignedCall('library.getArtists', [
            'user'  => $user,
            'limit' => $limit,
            'page'  => $page,
        ]);

        if (!isset($response['artists']['artist'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return ArtistInfo::fromApi($data);
            },
            $response['artists']['artist']
        );
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Model;

final class SongInfo
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var string|null
     */
    private $mbid;

    /**
     * @var int|null
     */
    private $duration;

    /**
     * @var Artist|null
     */
    private $artist;

    /**
     * @var int
     */
    private $listeners;

    /**
     * @var int
     */
    private $playcount;

    /**
     * @var Tag[]
     */
    private $topTags;

    /**
     * @param string      $name
     * @param string|null $mbid
     * @param int|null    $duration
     * @param Artist|null $artist
     * @param int         $listeners
     * @param int         $playcount
     * @param Tag[]       $topTags
     */
    public function __construct(
        string $name,
        ?string $mbid,
        ?int $duration,
        ?Artist $artist,
        int $listeners,
        int $playcount,
        array $topTags
    ) {
        $this->name      = $name;
        $this->mbid      = $mbid;
        $this->duration  = $duration;
        $this->artist    = $artist;
        $this->listeners = $listeners;
        $this->playcount = $playcount;
        $this->topTags   = $topTags;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string|null
     */
    public function getMbid(): ?string
    {
        return $this->mbid;
    }

    /**
     * @return int|null
     */
    public function getDuration(): ?int
    {
        return $this->duration;
    }

    /**
     * @return Artist|null
     */
    public function getArtist(): ?Artist
    {
        return $this->artist;
    }

    /**
     * @return int
     */
    public function getListeners(): int
    {
        return $this->listeners;
    }

    /**
     * @return int
     */
    public function getPlaycount(): int
    {
        return $this->playcount;
    }

    /**
     * @return Tag[]
     */
    public function getTopTags(): array
    {
        return $this->topTags;
    }

    /**
     * @param array $data
     *
     * @return SongInfo
     */
    public static function fromApi(array $data): self
    {
        $tags = self::createTagsFromApi($data);

        $artist = null;

        if (isset($data['artist'])) {
            if (\is_array($data['artist'])) {
                $artist = Artist::fromApi($data['artist']);
            } else {
                $artist = new Artist($data['artist'], null, [], null);
            }
        }

        return new self(
            $data['name'],
            $data['mbid'] ?? null,
            isset($data['duration']) ? (int) $data['duration'] : null,
            $artist,
            isset($data['listeners']) ? (int) $data['listeners'] : 0,
            isset($data['playcount']) ? (int) $data['playcount'] : 0,
            $tags
        );
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createTagsFromApi(array $data): array
    {
        $tags = [];

        if (isset($data['toptags']['tag'])) {
            foreach ((array) $data['toptags']['tag'] as $tag) {
                $tags[] = Tag::fromApi($tag);
            }
        }

        return $tags;
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Crawler;

use Core23\LastFm\Connection\ConnectionInterface;
use Core23\LastFm\Exception\CrawlException;
use Core23\LastFm\Model\Event;
use Core23\LastFm\Model\Image;
use Core23\LastFm\Model\Venue;
use Core23\LastFm\Model\VenueAddress;
use DateTime;
use Exception;
use Symfony\Component\DomCrawler\Crawler;

abstract class AbstractCrawler
{
    public const URL_PREFIX = 'http://last.fm';

    public const NEWLINE = "\n";

    /**
     * @var ConnectionInterface
     */
    private $connection;

    /**
     * @param ConnectionInterface $connection
     */
    public function __construct(ConnectionInterface $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Crawles a url.
     *
     * @param string $url
     * @param array  $params
     *
     * @return Crawler|null
     */
    final protected function crawl(string $url, array $params = []): ?Crawler
    {
        if ($content = $this->connection->getPageBody($url, $params)) {
            return new Crawler($content);
        }

        return null;
    }

    /**
     * @param Crawler  $node
     * @param DateTime $datetime
     *
     * @return Event
     */
    final protected function parseEvent(Crawler $node, DateTime $datetime = null): Event
    {
        $eventNode = $node->filter('.events-list-item-event--title a');

        $url = $this->parseUrl($eventNode);

        if (null === $url) {
            throw new CrawlException('Error parsing event id.');
        }

        $id = (int) preg_replace('/.*\/(\d+)+.*/', '$1', $url);

        if (0 === $id) {
            throw new CrawlException('Error parsing event id.');
        }

        if (null === $datetime) {
            try {
                $datetime = new DateTime($node->filter('time')->attr('datetime'));
            } catch (Exception $exception) {
                throw new CrawlException('Error reading event date', $exception->getCode(), $exception);
            }
        }

        $venue = $this->parseVenue($node->filter('.events-list-item-venue'));

        return new Event(
            $id,
            $this->parseString($eventNode) ?? '',
            $datetime,
            $url,
            $venue
        );
    }

    /**
     * @param Crawler $node
     *
     * @return Venue|null
     */
    final protected function parseVenue(Crawler $node): ?Venue
    {
        $title   = $this->parseString($node->filter('.events-list-item-venue--title'));

        if (null === $title) {
            return null;
        }

        $city    = $this->parseString($node->filter('.events-list-item-venue--city'));
        $country = $this->parseString($node->filter('.events-list-item-venue--country'));

        return new Venue($title, null, null, new VenueAddress(
            null,
            null,
            $city,
            $country
        ));
    }

    /**
     * Parses a url node.
     *
     * @param Crawler $node
     * @param string  $attr
     *
     * @return string|null
     */
    final protected function parseUrl(Crawler $node, string $attr = 'href'): ?string
    {
        if (0 === $node->count()) {
            return null;
        }

        if ($url = $node->attr($attr)) {
            return preg_replace('/^\//', static::URL_PREFIX.'/', $url);
        }

        return null;
    }

    /**
     * Parses an image node.
     *
     * @param Crawler $node
     *
     * @return Image|null
     */
    final protected function parseImage(Crawler $node): ?Image
    {
        $src = $this->parseUrl($node, 'src');

        if (!$src) {
            return null;
        }

        return new Image($src);
    }

    /**
     * Parses a string node.
     *
     * @param Crawler $node
     * @param bool    $multiline
     *
     * @return string|null
     */
    final protected function parseString(Crawler $node, bool $multiline = false): ?string
    {
        if (0 === $node->count()) {
            return null;
        }

        $content = $node->attr('content');

        if (null === $content) {
            if ($multiline) {
                $content = $node->html();
                $content = (string) preg_replace('/<p[^>]*?>/', '', $content);
                $content = str_replace('</p>', static::NEWLINE, $content);
                $content = (string) preg_replace('/<br\s?\/?>/i', static::NEWLINE, $content);
            } else {
                $content = $node->text();
            }
        }

        return trim(strip_tags($content));
    }

    /**
     * Parses a date note.
     *
     * @param Crawler $node
     *
     * @return DateTime|null
     */
    final protected function parseDate(Crawler $node): ?DateTime
    {
        $content = $this->parseString($node);

        if (null !== $content) {
            return new DateTime($content);
        }

        return null;
    }
}


<?php
namespace Paradox;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Autoloader
 * A simple script to autoload the class files for the library. The project is structured to be PSR-0 compliant, so you can also use your own
 * autoloader.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class Autoloader
{
    /**
     * Class file extension.
     */
    const EXTENSION = '.php';

    private static $dirRoot = null;

    /**
     * Initialise the autoloader by registering it.
     * @return void
     */
    public static function init()
    {
        spl_autoload_register(__NAMESPACE__ . '\Autoloader::load');

        self::$dirRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
    }

    /**
     * Search for the class's file and require it.
     * @param string $className The name of the class.
     */
    public static function load($className)
    {
        $className = str_replace(__NAMESPACE__, '', $className);
        $className = str_replace("\\", DIRECTORY_SEPARATOR, $className);

        if (file_exists(self::$dirRoot . $className . self::EXTENSION)) {
            require_once self::$dirRoot . $className . self::EXTENSION;
        }
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Connection;

use Core23\LastFm\Exception\ApiException;
use Http\Client\Exception;
use Http\Client\HttpClient;
use Http\Message\MessageFactory;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

final class HTTPlugConnection implements ConnectionInterface
{
    /**
     * @var HttpClient
     */
    private $client;

    /**
     * @var MessageFactory
     */
    private $messageFactory;

    /**
     * @var string
     */
    private $endpoint;

    /**
     * Initialize client.
     *
     * @param HttpClient     $client
     * @param MessageFactory $messageFactory
     * @param string         $endpoint
     */
    public function __construct(HttpClient $client, MessageFactory $messageFactory, string $endpoint = ConnectionInterface::DEFAULT_ENDPOINT)
    {
        $this->client         = $client;
        $this->messageFactory = $messageFactory;
        $this->endpoint       = $endpoint;
    }

    /**
     * {@inheritdoc}
     */
    public function getPageBody(string $url, array $params = [], string $method = 'GET'): ?string
    {
        $request  = $this->createRequest($method, $url, $params);

        try {
            $response = $this->client->sendRequest($request);
        } catch (Exception $e) {
            throw new ApiException('Error fetching page body', $e->getCode(), $e->getMessage());
        }

        if ($response->getStatusCode() >= 400) {
            return null;
        }

        return $response->getBody()->getContents();
    }

    /**
     * {@inheritdoc}
     */
    public function call(string $url, array $params = [], string $method = 'GET'): array
    {
        $params  = array_merge($params, ['format' => 'json']);
        $request = $this->createRequest($method, $this->endpoint, $params);

        try {
            $response = $this->client->sendRequest($request);

            // Parse response
            return $this->parseResponse($response);
        } catch (ApiException $e) {
            throw $e;
        } catch (\Exception $e) {
            throw new ApiException('Technical error occurred.', 500, $e);
        } catch (Exception $e) {
            throw new ApiException('Technical error occurred.', $e->getCode(), $e);
        }
    }

    /**
     * @param string $method
     * @param string $url
     * @param array  $params
     *
     * @return RequestInterface
     */
    private function createRequest(string $method, string $url, array $params): RequestInterface
    {
        $query = http_build_query($params);

        if ('POST' === $method) {
            return $this->messageFactory->createRequest($method, $url, [], $query);
        }

        return $this->messageFactory->createRequest($method, $url.'?'.$query);
    }

    /**
     * @param ResponseInterface $response
     *
     * @throws ApiException
     *
     * @return array
     */
    private function parseResponse(ResponseInterface $response): array
    {
        $array = json_decode($response->getBody()->getContents(), true);

        if (\is_array($array) && \array_key_exists('error', $array) && \array_key_exists('message', $array)) {
            throw new ApiException($array['message'], $array['error']);
        }

        return $array;
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Model;

final class AlbumInfo
{
    /**
     * @var string|null
     */
    private $name;

    /**
     * @var Artist|null
     */
    private $artist;

    /**
     * @var string|null
     */
    private $mbid;

    /**
     * @var string|null
     */
    private $url;

    /**
     * @var Image[]
     */
    private $images;

    /**
     * @var int
     */
    private $listeners;

    /**
     * @var int
     */
    private $playcount;

    /**
     * @var Song[]
     */
    private $tracks;

    /**
     * @var Tag[]
     */
    private $tags;

    /**
     * @var string|null
     */
    private $wikiSummary;

    /**
     * @param string|null $name
     * @param Artist|null $artist
     * @param string|null $mbid
     * @param string|null $url
     * @param Image[]     $images
     * @param int         $listeners
     * @param int         $playcount
     * @param Song[]      $tracks
     * @param Tag[]       $tags
     * @param string|null $wikiSummary
     */
    public function __construct(
        ?string $name,
        ?Artist $artist,
        ?string $mbid,
        ?string $url,
        array $images,
        int $listeners,
        int $playcount,
        array $tracks,
        array $tags,
        ?string $wikiSummary
    ) {
        $this->name         = $name;
        $this->artist       = $artist;
        $this->mbid         = $mbid;
        $this->url          = $url;
        $this->images       = $images;
        $this->listeners    = $listeners;
        $this->playcount    = $playcount;
        $this->tracks       = $tracks;
        $this->tags         = $tags;
        $this->wikiSummary  = $wikiSummary;
    }

    /**
     * @return string|null
     */
    public function getName(): ?string
    {
        return $this->name;
    }

    /**
     * @return Artist|null
     */
    public function getArtist(): ?Artist
    {
        return $this->artist;
    }

    /**
     * @return string|null
     */
    public function getMbid(): ?string
    {
        return $this->mbid;
    }

    /**
     * @return string|null
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }

    /**
     * @return Image[]
     */
    public function getImage(): array
    {
        return $this->images;
    }

    /**
     * @return int
     */
    public function getListeners(): int
    {
        return $this->listeners;
    }

    /**
     * @return int
     */
    public function getPlaycount(): int
    {
        return $this->playcount;
    }

    /**
     * @return Song[]
     */
    public function getTracks(): array
    {
        return $this->tracks;
    }

    /**
     * @return Tag[]
     */
    public function getTags(): array
    {
        return $this->tags;
    }

    /**
     * @return string|null
     */
    public function getWikiSummary(): ?string
    {
        return $this->wikiSummary;
    }

    /**
     * @param array $data
     *
     * @return AlbumInfo
     */
    public static function fromApi(array $data): self
    {
        $images = self::createImagesFromApi($data);
        $tracks = self::createTracksFromApi($data);
        $tags   = self::createTagsFromApi($data);

        return new self(
            $data['name'],
            new Artist($data['artist'], null, [], null),
            $data['mbid'] ?? null,
            $data['url'] ?? null,
            $images,
            $data['listeners'] ?? 0,
            $data['playcount'] ?? 0,
            $tracks,
            $tags,
            $data['wiki']['summary'] ?? null
        );
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createImagesFromApi(array $data): array
    {
        $images = [];

        if (\array_key_exists('image', $data)) {
            foreach ((array) $data['image'] as $image) {
                $images[] = new Image($image['#text']);
            }
        }

        return $images;
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createTracksFromApi(array $data): array
    {
        $tracks = [];

        if (\array_key_exists('tracks', $data) && \array_key_exists('track', $data['tracks'])) {
            foreach ((array) $data['tracks']['track'] as $track) {
                $tracks[] = Song::fromApi($track);
            }
        }

        return $tracks;
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createTagsFromApi(array $data): array
    {
        $tags = [];

        if (\array_key_exists('tags', $data) && \array_key_exists('tag', $data['tags'])) {
            foreach ((array) $data['tags']['tag'] as $tag) {
                $tags[] = Tag::fromApi($tag);
            }
        }

        return $tags;
    }
}


<?php
namespace asbamboo\framework\template\extension;

use asbamboo\framework\template\Template;
use asbamboo\template\Extension;
use asbamboo\template\Functions;
use asbamboo\security\user\token\UserTokenInterface;

/**
 *
 * @author 李春寅 <licy2013@aliyun.com>
 * @since 2018年9月4日
 */
class AuthorizationExtension extends Extension
{
    /**
     *
     * @var Template
     */
    private $Template;

    /**
     *
     * @param Template $Template
     */
    public function __construct(Template $Template)
    {
        $this->Template = $Template;
    }

    /**
     *
     * {@inheritDoc}
     * @see Extension::getFunctions()
     */
    public function getFunctions()
    {
        return [
            new Functions('has_roles', [$this, 'hasRoles']),
        ];
    }

    /***
     * 判断当前用户是否包含这些角色
     *
     * @param string ...$roles
     * @return boolean
     */
    public function hasRoles(string ...$roles)
    {
        /**
         *
         * @var UserTokenInterface $UserToken
         */
        $UserToken      = $this->Template->getContainer()->get(UserTokenInterface::class);
        $user_roles     = $UserToken->getUser()->getRoles();
        $diffed_roles   = array_diff($user_roles, $roles);
        return count($user_roles) > count($diffed_roles);
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Model;

final class Tag
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var string|null
     */
    private $url;

    /**
     * @var int|null
     */
    private $count;

    /**
     * @param string      $name
     * @param string|null $url
     * @param int|null    $count
     */
    public function __construct(string $name, ?string $url, ?int $count)
    {
        $this->name  = $name;
        $this->url   = $url;
        $this->count = $count;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string|null
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }

    /**
     * @return int|null
     */
    public function getCount(): ?int
    {
        return $this->count;
    }

    /**
     * @param array $data
     *
     * @return Tag
     */
    public static function fromApi(array $data): self
    {
        return new self(
            $data['name'],
            $data['url'] ?? null,
            isset($data['count']) ? (int) $data['count'] : null
        );
    }
}


<?php
/**
 * JBZoo PHPUnit
 *
 * This file is part of the JBZoo CCK package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package    PHPUnit
 * @license    MIT
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
 * @link       https://github.com/JBZoo/PHPUnit
 * @author     Denis Smetannikov <denis@jbzoo.com>
 */

namespace JBZoo\PHPUnit;

use JBZoo\Data\Data;
use JBZoo\Utils\Sys;
use JBZoo\Utils\Str;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\Clover;
use SebastianBergmann\CodeCoverage\Report\Html\Facade;
use SebastianBergmann\CodeCoverage\Report\PHP;
use JBZoo\Utils\Env;

/**
 * Class CovCatcher
 *
 * @package JBZoo\PHPUnit
 *
 * @codeCoverageIgnore
 */
class CovCatcher
{
    const MODE_REQUIRE      = 'require';
    const MODE_REQUIRE_ONCE = 'require_once';

    protected $_isStart = false;

    /**
     * @var CodeCoverage
     */
    protected $_coverage;

    /**
     * @var array
     */
    protected $_default = [
        'cov'        => true,
        'xml'        => false,
        'html'       => false,
        'src'        => './src',
        'build_xml'  => './build/coverage_xml',
        'build_cov'  => './build/coverage_cov',
        'build_html' => './build/coverage_html',
    ];

    /**
     * @var Data
     */
    protected $_config;

    /**
     * @var string
     */
    protected $_hash;

    /**
     * CovCatcher constructor.
     *
     * @param string $testName
     * @param array  $options
     * @throws Exception
     */
    public function __construct($testName = null, array $options = [])
    {
        if (!class_exists(Data::class)) {
            throw new Exception('jbzoo/data required for CovCatcher');
        }

        if (!class_exists(Env::class)) {
            throw new Exception('jbzoo/utils required for CovCatcher');
        }

        $this->_initConfig($options);

        $this->_hash = $testName . '__' . str_replace('.', '', uniqid('', true));

        if (Sys::hasXdebug()) {
            $covFilter = new Filter();
            $covFilter->addDirectoryToWhitelist($this->_config->get('src'));
            $this->_coverage = new CodeCoverage(null, $covFilter);
        }
    }

    /**
     * @param string $filename
     * @param string $mode
     * @return mixed
     * @throws Exception
     */
    public function includeFile($filename, $mode = self::MODE_REQUIRE)
    {
        $this->_start();

        $realpath = realpath($filename);
        if (!file_exists($realpath)) {
            throw new Exception("Included file not found: \"{$filename}\"");
        }

        if (self::MODE_REQUIRE === $mode) {
            $result = require $realpath;

        } elseif (self::MODE_REQUIRE_ONCE === $mode) {
            $result = require_once $realpath;

        } else {
            throw new Exception("Undefined mode to include file: \"{$filename}\"");
        }

        $this->_stop();

        return $result;
    }

    /**
     * @param null|string $testName
     * @return string
     */
    protected function _getPrefix($testName = null)
    {
        if (null === $testName) {
            $objects = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
            foreach ($objects as $object) {
                if (isset($object['object']) && $object['object'] instanceof TestCase) {
                    $testName = $object['class'] . '_' . $object['function'];
                    break;
                }
            }
        }

        $testName = str_replace(__NAMESPACE__ . '\\', '', $testName);
        $testName = Str::splitCamelCase($testName, '_', true);
        $testName = preg_replace('/^test_/', '', $testName);
        $testName = preg_replace('/_test$/', '', $testName);
        $testName = str_replace('_test_test_', '_', $testName);
        $testName = str_replace(['/', '\\', '_', '-'], '', $testName);
        $testName = strtolower($testName);

        if (!$testName) {
            $testName = uniqid('', true);
            $testName = str_replace('.', '', $testName);
        }

        return $testName;
    }

    /**
     * Save report
     */
    public function __destruct()
    {
        $this->_stop();
        $this->_createReports();
    }

    /**
     * Start coverage proccess
     */
    protected function _start()
    {
        if (!$this->_isStart && $this->_coverage) {
            $this->_isStart = true;
            $this->_coverage->start($this->_hash, true);
        }
    }

    /**
     * Stop or pause coverage proccess
     */
    protected function _stop()
    {
        if ($this->_isStart && $this->_coverage) {
            $this->_isStart = false;
            $this->_coverage->stop();
        }
    }

    /**
     * Stop or pause coverage proccess
     */
    protected function _createReports()
    {
        if (!$this->_coverage) {
            return;
        }

        $reportXmlDir = $this->_config->get('build_xml');
        if ($this->_config->get('xml', true, 'bool')) {
            $this->_checkDir($reportXmlDir);
            $report = new Clover();
            $report->process($this->_coverage, $reportXmlDir . '/' . $this->_hash . '.xml');
        }

        $reportCovDir = $this->_config->get('build_cov');
        if ($this->_config->get('cov', false, 'bool')) {
            $this->_checkDir($reportCovDir);
            $report = new PHP();
            $report->process($this->_coverage, $reportCovDir . '/' . $this->_hash . '.cov');
        }

        $reportHtmlDir = $this->_config->get('build_html');
        if ($this->_config->get('html', false, 'bool')) {
            $this->_checkDir($reportHtmlDir);
            $report = new Facade();
            $report->process($this->_coverage, $reportHtmlDir . '/' . $this->_hash);
        }
    }

    /**
     * @param string $dirPath
     */
    protected function _checkDir($dirPath)
    {
        if (!is_dir($dirPath)) {
            mkdir($dirPath, 0777, true);
        }
    }

    /**
     * Prepare and init config
     *
     * @param array $options
     */
    protected function _initConfig(array $options)
    {
        $options = array_filter($options, function ($option) {
            return null !== $option;
        });

        $this->_config = new Data(array_merge($this->_default, $options));
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Service;

use Core23\LastFm\Builder\ScrobbeBuilder;
use Core23\LastFm\Builder\SimilarTrackBuilder;
use Core23\LastFm\Builder\TrackInfoBuilder;
use Core23\LastFm\Builder\TrackTagsBuilder;
use Core23\LastFm\Builder\TrackTopTagsBuilder;
use Core23\LastFm\Client\ApiClientInterface;
use Core23\LastFm\Model\NowPlaying;
use Core23\LastFm\Model\Song;
use Core23\LastFm\Model\SongInfo;
use Core23\LastFm\Model\Tag;
use Core23\LastFm\Session\SessionInterface;
use Core23\LastFm\Util\ApiHelper;
use InvalidArgumentException;

final class TrackService implements TrackServiceInterface
{
    /**
     * @var ApiClientInterface
     */
    private $client;

    /**
     * @param ApiClientInterface $client
     */
    public function __construct(ApiClientInterface $client)
    {
        $this->client = $client;
    }

    /**
     * {@inheritdoc}
     */
    public function addTags(SessionInterface $session, string $artist, string $track, array $tags): void
    {
        $count = \count($tags);

        if (0 === $count) {
            throw new InvalidArgumentException('No tags given');
        }
        if ($count > 10) {
            throw new InvalidArgumentException('A maximum of 10 tags is allowed');
        }

        array_filter($tags, static function ($tag) {
            if (null === $tag || !\is_string($tag)) {
                throw new InvalidArgumentException(sprintf('Invalid tag given'));
            }
        });

        $this->client->signedCall('track.addTags', [
            'artist' => $artist,
            'track'  => $track,
            'tags'   => implode(',', $tags),
        ], $session, 'POST');
    }

    /**
     * {@inheritdoc}
     */
    public function getCorrection(string $artist, string $track): ?Song
    {
        $response = $this->client->unsignedCall('track.getCorrection', [
            'artist' => $artist,
            'track'  => $track,
        ]);

        if (!isset($response['corrections']['correction']['track'])) {
            return null;
        }

        return Song::fromApi($response['corrections']['correction']['track']);
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo(TrackInfoBuilder $builder): ?SongInfo
    {
        $response = $this->client->unsignedCall('track.getInfo', $builder->getQuery());

        if (!isset($response['track'])) {
            return null;
        }

        return SongInfo::fromApi($response['track']);
    }

    /**
     * {@inheritdoc}
     */
    public function getSimilar(SimilarTrackBuilder $builder): array
    {
        $response = $this->client->unsignedCall('track.getSimilar', $builder->getQuery());

        if (!isset($response['similartracks']['track'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return SongInfo::fromApi($data);
            },
            $response['similartracks']['track']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTags(TrackTagsBuilder $builder): array
    {
        $response = $this->client->unsignedCall('track.getTags', $builder->getQuery());

        if (!isset($response['tags']['tag'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Tag::fromApi($data);
            },
            $response['tags']['tag']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTopTags(TrackTopTagsBuilder $builder): array
    {
        $response = $this->client->unsignedCall('track.getTopTags', $builder->getQuery());

        if (!isset($response['toptags']['tag'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Tag::fromApi($data);
            },
            $response['toptags']['tag']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function love(SessionInterface $session, string $artist, string $track): void
    {
        $this->client->signedCall('track.love', [
            'artist' => $artist,
            'track'  => $track,
        ], $session, 'POST');
    }

    /**
     * {@inheritdoc}
     */
    public function removeTag(SessionInterface $session, string $artist, string $track, string $tag): void
    {
        $this->client->signedCall('track.removeTag', [
            'artist' => $artist,
            'track'  => $track,
            'tag'    => $tag,
        ], $session, 'POST');
    }

    /**
     * {@inheritdoc}
     */
    public function scrobble(SessionInterface $session, ScrobbeBuilder $builder): void
    {
        $count = $builder->count();

        if (0 === $count) {
            return;
        }
        if ($count > 10) {
            throw new InvalidArgumentException('A maximum of 50 tracks is allowed');
        }

        $this->client->signedCall('album.scrobble', $builder->getQuery(), $session, 'POST');
    }

    /**
     * {@inheritdoc}
     */
    public function search(string $track, int $limit = 50, int $page = 1): array
    {
        $response = $this->client->unsignedCall('track.search', [
            'track' => $track,
            'limit' => $limit,
            'page'  => $page,
        ]);

        if (!isset($response['results']['trackmatches']['track'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return SongInfo::fromApi($data);
            },
            $response['results']['trackmatches']['track']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function unlove(SessionInterface $session, string $artist, string $track): void
    {
        $this->client->signedCall('track.love', [
            'artist' => $artist,
            'track'  => $track,
        ], $session, 'POST');
    }

    /**
     * {@inheritdoc}
     */
    public function updateNowPlaying(SessionInterface $session, NowPlaying $nowPlaying): void
    {
        $this->client->signedCall('track.updateNowPlaying', $nowPlaying->toArray(), $session, 'POST');
    }
}


<?php
namespace Paradox\pod;
use Paradox\Toolbox;
use Paradox\AModel;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Vertex pod
 * Represents an ArangoDB vertex. Implements IObserver to listen to events from the Pod Manager.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class Vertex extends Document
{
    /**
     * Instantiates the pod.
     * @param Toolbox $toolbox A reference to the toolbox that owns this pod.
     * @param array   $data    Any data that we wish to instantiate this pod with.
     * @param string  $new     Whether this pod should be marked as new (never been saved to the server).
     */
    public function __construct(Toolbox $toolbox, array $data = array(), $new = true)
    {
        parent::__construct($toolbox, "vertex", $data, $new);
    }

    /**
     * Create an edge connecting this vertex to another vertex.
     * @param  AModel $to    The model we wish to connect to.
     * @param  string $label An optional label for this edge.
     * @return Edge
     */
    public function relateTo(AModel $to, $label = null)
    {
        $edge = $this->_toolbox->getPodManager()->dispense("edge", $label);
        $edge->setTo($to);
        $edge->setFrom($this->getModel());

        return $edge;
    }

    /**
     * Get all inbound edges to this vertex. The edges can be filtered by their labels and AQL.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getInboundEdges($label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        return $this->_toolbox->getGraphManager()->getInboundEdges($this->getId(), $label, $aql, $params, $placeholder);
    }

    /**
     * Get all outbound edges to this vertex. The edges can be filtered by their labels and AQL.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getOutboundEdges($label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        return $this->_toolbox->getGraphManager()->getOutboundEdges($this->getId(), $label, $aql, $params, $placeholder);
    }

    /**
     * Get all edges connected to this vertex. The edges can be filtered by their labels and AQL.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getEdges($label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        return $this->_toolbox->getGraphManager()->getEdges($this->getId(), $label, $aql, $params, $placeholder);
    }

    /**
     * Get the neighbour vertices connected to this vertex via some edge. The vertices and their connecting edges can be filtered by AQL.
     * @param  string       $direction   "in" for inbound neighbours, "out" for outbound neighbours and "any" for all neighbours.
     * @param  string|array $labels      A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string       $aql         An optional AQL fragment if we want to filter the edges or vertices, for example:
     *                                   FILTER doc.edge.someproperty == "somevalue" && doc.vertex.anotherproperty == "anothervalue"
     * @param  array        $params      An optional associative array containing parameters to bind to the query.
     * @param  string       $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getNeighbours($direction = "any", $labels = null, $aql = "", $params = array(), $placeholder = "doc")
    {
       return $this->_toolbox->getGraphManager()->getNeighbours($this->getId(), $direction, $labels, $aql, $params, $placeholder);
    }

    /**
     * Returns an ArangoDB-PHP vertex object representing this vertex.
     * @return \triagens\ArangoDb\Vertex
     */
    public function toDriverDocument()
    {
        $vertex = new \triagens\ArangoDb\Vertex;

        foreach ($this->_data as $key => $value) {
            $vertex->set($key, $value);
        }

        if ($this->_id) {
            $vertex->setInternalId($this->_id);
        }

        if ($this->_key) {
            $vertex->setInternalKey($this->_key);
        }

        if ($this->_rev) {
            $vertex->setRevision($this->_rev);
        }

        return $vertex;
    }

}


<?php

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Client;

use Core23\LastFm\Connection\ConnectionInterface;
use Core23\LastFm\Exception\ApiException;
use Core23\LastFm\Exception\NotFoundException;
use Core23\LastFm\Session\SessionInterface;

final class ApiClient implements ApiClientInterface
{
    /**
     * @var ConnectionInterface
     */
    private $connection;

    /**
     * @var string
     */
    private $apiKey;

    /**
     * @var string
     */
    private $sharedSecret;

    /**
     * @param ConnectionInterface $connection
     * @param string              $apiKey
     * @param string              $sharedSecret
     */
    public function __construct(ConnectionInterface $connection, string $apiKey, string $sharedSecret)
    {
        $this->connection   = $connection;
        $this->apiKey       = $apiKey;
        $this->sharedSecret = $sharedSecret;
    }

    /**
     * {@inheritdoc}
     */
    public function getApiKey(): string
    {
        return $this->apiKey;
    }

    /**
     * {@inheritdoc}
     */
    public function getSharedSecret(): string
    {
        return $this->sharedSecret;
    }

    /**
     * {@inheritdoc}
     */
    public function signedCall(string $method, array $params = [], SessionInterface $session = null, $requestMethod = 'GET'): array
    {
        // Call parameter
        $callParams = [
            'method'  => $method,
            'api_key' => $this->apiKey,
        ];

        // Add session key
        if (null !== $session) {
            $callParams['sk'] = $session->getKey();
        }

        $params = array_merge($callParams, $params);
        $params = $this->filterNull($params);
        $params = $this->encodeUTF8($params);

        // Sign parameter
        $params['api_sig'] = $this->signParams($params);

        return $this->call($method, $params);
    }

    /**
     * {@inheritdoc}
     */
    public function unsignedCall(string $method, array $params = [], string $requestMethod = 'GET'): array
    {
        // Call parameter
        $callParameter = [
            'method'  => $method,
            'api_key' => $this->apiKey,
        ];

        $params = array_merge($callParameter, $params);
        $params = $this->filterNull($params);
        $params = $this->encodeUTF8($params);

        return $this->call($method, $params);
    }

    /**
     * Filter null values.
     *
     * @param array $object
     *
     * @return array
     */
    private function filterNull(array $object): array
    {
        return array_filter($object, static function ($val) {
            return null !== $val;
        });
    }

    /**
     * Converts any string or array of strings to UTF8.
     *
     * @param mixed|mixed[] $object a String or an array
     *
     * @return mixed|mixed[] UTF8-string or array
     */
    private function encodeUTF8($object)
    {
        if (\is_array($object)) {
            return array_map([$this, 'encodeUTF8'], $object);
        }

        return mb_convert_encoding((string) $object, 'UTF-8', 'auto');
    }

    /**
     * @param array $params
     *
     * @return string
     */
    private function signParams(array $params): string
    {
        ksort($params);

        $signature = '';
        foreach ($params as $name => $value) {
            $signature .= $name.$value;
        }
        $signature .= $this->sharedSecret;

        return md5($signature);
    }

    /**
     * @param string $method
     * @param array  $params
     *
     * @return array
     */
    private function call(string $method, array $params): array
    {
        try {
            return $this->connection->call($method, $params);
        } catch (ApiException $e) {
            if (6 === (int) $e->getCode()) {
                throw new NotFoundException('No entity was found for your request.', $e->getCode(), $e);
            }

            throw $e;
        }
    }
}


<?php
namespace Paradox\toolbox;
use Paradox\Toolbox;
use Paradox\exceptions\CollectionManagerException;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Collection manager
 * Manages collections, for example, creating and deleting collections and indices.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class CollectionManager
{
    /**
     * A reference to the toolbox.
     * @var Toolbox
     */
    private $_toolbox;

    /**
     * A small cache to store the index into to prevent too many trips to the server.
     * @var array()
     */
    private $_indexInfoCache = array();

    /**
     * Instantiates the finder.
     * @param Toolbox $toolbox
     */
    public function __construct(Toolbox $toolbox)
    {
        $this->_toolbox = $toolbox;
    }

    /**
     * Create a collection.
     * @param  string                     $name The name of the collection.
     * @throws CollectionManagerException
     * @return string
     */
    public function createCollection($name)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->create($name);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Delete a collection.
     * @param  string                     $name The name of the collection.
     * @throws CollectionManagerException
     */
    public function deleteCollection($name)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->delete($name);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Renames a collection.
     * @param  string                     $collection The collection we wish to rename.
     * @param  string                     $newName    The new name of the collection.
     * @throws CollectionManagerException
     */
    public function renameCollection($collection, $newName)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->rename($collection, $newName);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Deletes all the documents inside the collection, but leaves the indexes and metadata intact.
     * @param  string                     $collection The name of the collection.
     * @throws CollectionManagerException
     */
    public function wipe($collection)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->truncate($collection);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Get information about a collection.
     * @param  string                     $collection The name of the collection.
     * @throws CollectionManagerException
     * @return array
     */
    public function getCollectionInfo($collection)
    {
        try {
            $collection = $this->_toolbox->getCollectionHandler()->getProperties($collection);

            $result = $collection->getAll();

            switch ($result['type']) {
                case 2:
                    $result['type'] = "documents";
                    break;

                case 3:
                    $result['type'] = "edges";
                    break;
            }

            return $result;

        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Get statistics from a collection.
     * @param  string                     $collection The name of the collection.
     * @throws CollectionManagerException
     * @return array
     */
    public function getCollectionStatistics($collection)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->getFigures($collection);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Counts the number of documents in a collection.
     * @param  string                     $collection The collection.
     * @throws CollectionManagerException
     * @return integer
     */
    public function count($collection)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->count($collection);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * List all the collections on the server.
     * @param  boolean                    $excludeSystem Whether we want to include system collections in the list or not.
     * @param  boolean                    $includeInfo   Whether we want to include information about each collection. If false, only a list of collection names will be returned.
     * @throws CollectionManagerException
     * @return array
     */
    public function listCollections($excludeSystem = true, $includeInfo = false)
    {
        try {
            $result = $this->_toolbox->getCollectionHandler()->getAllCollections(array('excludeSystem' => (bool) $excludeSystem));

            if (empty($result)) {
                return array();
            }

            if (!$includeInfo) {
                return array_keys($result);
            } else {
                return $result;
            }
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Load a collection on the server.
     * @param  string                     $collection The name of the collection.
     * @throws CollectionManagerException
     */
    public function loadCollection($collection)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->load($collection);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Unload a collection on the server.
     * @param  string                     $collection The name of the collection.
     * @throws CollectionManagerException
     */
    public function unloadCollection($collection)
    {
        try {
            return $this->_toolbox->getCollectionHandler()->unload($collection);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Create a cap constraint on a collection.
     * @param  string                     $collection The name of the collection.
     * @param  int                        $size       The size of the cap constraint.
     * @link http://www.arangodb.org/manuals/current/IndexCapHttp.html
     * @throws CollectionManagerException
     * @return int                        Id of the index created.
     */
    public function createCapConstraint($collection, $size)
    {
        try {
            $result = $this->_toolbox->getCollectionHandler()->createCapConstraint($collection, $size);

            return $this->_toolbox->parseIdForKey($result['id']);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Create a geo index on a collection.
     * @param  string                     $collection The name of the collection.
     * @param  array|string               $fields     An array of 2 fields representing the latitude and longitude, or 1 field representing a list attribute.
     * @param  boolean                    $geoJson    Whether to use geoJson or not.
     * @param  boolean                    $constraint Whether this is a constraint or not.
     * @param  boolean                    $ignoreNull Whether to ignore null.
     * @link http://www.arangodb.org/manuals/current/IndexGeoHttp.html
     * @throws CollectionManagerException
     * @return int                        Id of the index created.
     */
    public function createGeoIndex($collection, $fields, $geoJson = null, $constraint = null, $ignoreNull = null)
    {
        if (!is_array($fields)) {
            $fields = array($fields);
        }

        try {
            $result = $this->_toolbox->getCollectionHandler()->createGeoIndex($collection, $fields, $geoJson, $constraint, $ignoreNull);

            return $this->_toolbox->parseIdForKey($result['id']);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Create a hash index on a collection.
     * @param  string                     $collection The name of the collection.
     * @param  array|string               $fields     The array of fields or a string representing 1 field.
     * @param  boolean                    $unique     Whether the values in the index should be unique or not.
     * @link http://www.arangodb.org/manuals/current/IndexHashHttp.html
     * @throws CollectionManagerException
     * @return int                        Id of the index created.
     */
    public function createHashIndex($collection, $fields, $unique = null)
    {
        if (!is_array($fields)) {
            $fields = array($fields);
        }

        try {
            $result = $this->_toolbox->getCollectionHandler()->createHashIndex($collection, $fields, $unique);

            return $this->_toolbox->parseIdForKey($result['id']);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Create a fulltext index on a collection.
     * @param  string                     $collection The name of the collection.
     * @param  string                     $field      The field to index. Fulltext indices can currently only index one field.
     * @param  int                        $minLength  The minimum length of words to index.
     * @link http://www.arangodb.org/manuals/current/IndexFulltextHttp.html
     * @throws CollectionManagerException
     * @return int                        Id of the index created.
     */
    public function createFulltextIndex($collection, $field, $minLength = null)
    {
        if (!is_string($field)) {
            throw new CollectionManagerException("Fulltext indices can currently only index one field.");
        }

        $field = array($field);

        try {
            $result = $this->_toolbox->getCollectionHandler()->createFulltextIndex($collection, $field, $minLength);

            return $this->_toolbox->parseIdForKey($result['id']);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Create a skip-list index on a collection.
     * @param  string                     $collection The name of the collection.
     * @param  array|string               $fields     The array of fields or a string representing 1 field.
     * @param  bool                       $unique     Whether the index is unique or not.
     * @link http://www.arangodb.org/manuals/current/IndexSkiplistHttp.html
     * @throws CollectionManagerException
     * @return int                        Id of the index created.
     */
    public function createSkipListIndex($collection, $fields, $unique = null)
    {
        if (!is_array($fields)) {
            $fields = array($fields);
        }

        try {
            $result = $this->_toolbox->getCollectionHandler()->createSkipListIndex($collection, $fields, $unique);

            return $this->_toolbox->parseIdForKey($result['id']);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Delete an index.
     * @param  string                     $collection The collection the index is on.
     * @param  string                     $indexId    The id of the index we want to delete.
     * @throws CollectionManagerException
     * @return boolean
     */
    public function deleteIndex($collection, $indexId)
    {
        try {
            $this->_toolbox->getCollectionHandler()->dropIndex($collection . '/' . $indexId);

            //Invalidate the cached index info for this collection
            unset($this->_indexInfoCache[$collection]);

            return true;
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Get information about an index. Returns null if the index does not exist and returns an array of index information if it does.
     * @param  string     $collection The name of the collection.
     * @param  string     $indexId    The id of the index.
     * @return null|array
     */
    public function getIndexInfo($collection, $indexId)
    {
        $result = $this->listIndices($collection, true);

        return isset($result[$collection . '/' . $indexId]) ? $result[$collection . '/' . $indexId] : null;
    }

    /**
     * List the indices on a collection.
     * @param  string                     $collection  The name of the collection.
     * @param  boolean                    $includeInfo Whether we want information on each index. If false, only an array of index ids will be returned.
     * @throws CollectionManagerException
     * @return array
     */
    public function listIndices($collection, $includeInfo = false)
    {

        if (isset($this->_indexInfoCache[$collection])) {
            return $this->_indexInfoCache[$collection];
        }

        try {
            $result = $this->_toolbox->getCollectionHandler()->getIndexes($collection);
            $this->_indexInfoCache[$collection] = $result['identifiers'];

            if ($includeInfo) {
                return $this->_indexInfoCache[$collection];
            } else {
                return array_keys($this->_indexInfoCache[$collection]);
            }

        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new CollectionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Get the fields that are used to store geo coordinates for the first index for this collection.
     * @param  string          $collection The name of the collection
     * @throws FinderException
     * @return array|null
     */
    public function getGeoFieldsForAQL($collection)
    {
        $indices = $this->_toolbox->getCollectionManager()->listIndices($collection, true);

        foreach ($indices as $index => $info) {

            //If this is the first geo index we encounter
            if ($info['type'] == "geo1" || $info['type'] == "geo2") {
                return $info['fields'];
            }
        }

        return null;
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Model;

final class ArtistInfo
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var string|null
     */
    private $mbid;

    /**
     * @var Image[]
     */
    private $image;

    /**
     * @var string|null
     */
    private $url;

    /**
     * @var int
     */
    private $playcount;

    /**
     * @var string|null
     */
    private $bioSummary;

    /**
     * @var string|null
     */
    private $bioContent;

    /**
     * @var int
     */
    private $tagcount;

    /**
     * @var Tag[]
     */
    private $tags;

    /**
     * @param string      $name
     * @param string|null $mbid
     * @param Image[]     $image
     * @param string|null $url
     * @param int         $playcount
     * @param string|null $bioSummary
     * @param string|null $bioContent
     * @param int         $tagcount
     * @param Tag[]       $tags
     */
    public function __construct(
        string $name,
        ?string $mbid,
        array $image,
        ?string $url,
        int $playcount,
        ?string $bioSummary,
        ?string $bioContent,
        int $tagcount,
        array $tags
    ) {
        $this->name       = $name;
        $this->mbid       = $mbid;
        $this->image      = $image;
        $this->url        = $url;
        $this->playcount  = $playcount;
        $this->bioSummary = $bioSummary;
        $this->bioContent = $bioContent;
        $this->tagcount   = $tagcount;
        $this->tags       = $tags;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string|null
     */
    public function getMbid(): ?string
    {
        return $this->mbid;
    }

    /**
     * @return Image[]
     */
    public function getImage(): array
    {
        return $this->image;
    }

    /**
     * @return string|null
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }

    /**
     * @return int
     */
    public function getPlaycount(): int
    {
        return $this->playcount;
    }

    /**
     * @return string|null
     */
    public function getBioSummary(): ?string
    {
        return $this->bioSummary;
    }

    /**
     * @return string|null
     */
    public function getBioContent(): ?string
    {
        return $this->bioContent;
    }

    /**
     * @return int
     */
    public function getTagcount(): int
    {
        return $this->tagcount;
    }

    /**
     * @return Tag[]
     */
    public function getTags(): array
    {
        return $this->tags;
    }

    /**
     * @param array $data
     *
     * @return ArtistInfo
     */
    public static function fromApi(array $data): self
    {
        $images = self::createImagesFromApi($data);
        $tags   = self::createTagsFromApi($data);

        return new self(
            $data['name'],
            $data['mbid'] ?? null,
            $images,
            $data['url'] ?? null,
            isset($data['playcount']) ? (int) $data['playcount'] : 0,
            $data['bio']['summary'] ?? null,
            $data['bio']['content'] ?? null,
            isset($data['tagcount']) ? (int) $data['tagcount'] : 0,
            $tags
        );
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createImagesFromApi(array $data): array
    {
        $images = [];

        if (\array_key_exists('image', $data)) {
            foreach ((array) $data['image'] as $image) {
                $images[] = new Image($image['#text']);
            }
        }

        return $images;
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private static function createTagsFromApi(array $data): array
    {
        $tags = [];

        if (\array_key_exists('tags', $data) && \array_key_exists('tag', $data['tags'])) {
            foreach ((array) $data['tags']['tag'] as $tag) {
                $tags[] = Tag::fromApi($tag);
            }
        }

        return $tags;
    }
}