<?php

namespace Mlntn\Console\Commands;

use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\ProcessUtils;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\PhpExecutableFinder;

class Serve extends Command {

  /**
   * The console command name.
   *
   * @var string
   */
  protected $name = 'serve';

  /**
   * The console command description.
   *
   * @var string
   */
  protected $description = 'Serve the application on the PHP development server';

  /**
   * Execute the console command.
   *
   * @return void
   *
   * @throws \Exception
   */
  public function handle() {

    $host = $this->input->getOption('host');

    $port = $this->input->getOption('port');

    $base = ProcessUtils::escapeArgument(__DIR__);

    $binary = ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false));

    $docroot = $this->laravel->basePath() . '/public';

    $this->info("Lumen development server started on http://{$host}:{$port}/");

    if (defined('HHVM_VERSION')) {
      if (version_compare(HHVM_VERSION, '3.8.0') >= 0) {
        passthru("{$binary} -m server -v Server.Type=proxygen -v Server.SourceRoot={$base}/ -v Server.IncludeSearchPaths.docroot={$docroot} -v Server.IP={$host} -v Server.Port={$port} -v Server.DefaultDocument=server.php -v Server.ErrorDocument404=server.php");
      }
      else {
        throw new Exception("HHVM's built-in server requires HHVM >= 3.8.0.");
      }
    }
    else {
      passthru("{$binary} -S {$host}:{$port} -t '{$docroot}' {$base}/server.php");
    }
  }

  /**
   * Get the console command options.
   *
   * @return array
   */
  protected function getOptions() {
    return [
      ['host', null, InputOption::VALUE_OPTIONAL, 'The host address to serve the application on.', 'localhost'],
      ['port', null, InputOption::VALUE_OPTIONAL, 'The port to serve the application on.', 8000],
    ];
  }

}


<?php

namespace FbMessengerBot;

/**
 * Class Attachment
 *
 */
class Attachment
{
    /**
     * @var string
     */
    protected $type;

    /**
     * @var Payload
     */
    protected $payload;

    /**
     * Set type
     *
     * @param string $type Type
     */
    public function setType($type)
    {
        $this->type = $type;
    }

    /**
     * Get type
     *
     * @return string Type
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Set payload
     *
     * @param Payload $payload
     */
    public function setPayload($payload)
    {
        $model = new Payload();

        if (!empty($payload['url'])) {
            $model->setUrl($payload['url']);
        }

        if (!empty($payload['is_reusable'])) {
            $model->setIsReusable($payload['is_reusable']);
        }

        if (!empty($payload['template_type'])) {
            $model->setTemplateType($payload['template_type']);
        }

        if (!empty($payload['text'])) {
            $model->setText($payload['text']);
        }

        if (!empty($payload['attachment_id'])) {
            $model->setAttachmentId($payload['attachment_id']);
        }

        if (!empty($payload['buttons'])) {
            foreach ($payload['buttons'] as $button) {
                $model->setButton($button);
            }
        }

        $this->payload = $model;
    }

    /**
     * Get payload
     *
     * @return Payload
     */
    public function getPayload()
    {
        return $this->payload;
    }
}


<?php

namespace FbMessengerBot;

/**
 * Class Helper
 *
 */
class Helper
{
    /**
     * Object to array
     *
     * @param object $object Object
     * @param array $array Array
     *
     * @return array
     */
    public function objectToArray($object, $array = [])
    {
        $reflectionClass = new \ReflectionClass(get_class($object));

        foreach ($reflectionClass->getProperties() as $property) {
            $property->setAccessible(true);

            $name = trim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $property->getName())), '_');

            if (empty($property->getValue($object))) {
                continue;
            } else if (is_object($property->getValue($object))) {
                $array[$name] = $this->objectToArray($property->getValue($object));
            } else if (is_array($property->getValue($object))) {
                foreach ($property->getValue($object) as $key => $value) {
                    if (is_object($value)) {
                        $array[$name][] = $this->objectToArray($value);
                    }
                }
            } else {
                $array[$name] = $property->getValue($object);
            }

            $property->setAccessible(false);
        }

        return $array;
    }
}


<?php

namespace FbMessengerBot;

/**
 * Class Message
 *
 */
class Message
{
    /**
     * @var string
     */
    protected $text;

    /**
     * @var array
     */
    protected $quickReplies;

    /**
     * @var Attachment
     */
    protected $attachment;

    /**
     * Set text
     *
     * @param string $text Text
     */
    public function setText($text)
    {
        $this->text = $text;
    }

    /**
     * Get text
     *
     * @return string Text
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set attachment
     *
     * @param string $type Type
     * @param array $payload Payload
     */
    public function setAttachment($type, $payload)
    {
        $attachment = new Attachment();

        $attachment->setType($type);
        $attachment->setPayload($payload);

        $this->attachment = $attachment;
    }

    /**
     * Get attachment
     *
     * @return Attachment
     */
    public function getAttachment()
    {
        return $this->attachment;
    }

    /**
     * Set quick replies
     *
     * @param array $quickReplie Quick replie
     */
    public function setQuickReplies($quickReplie)
    {
        $model = new QuickReplie();

        $type = !empty($quickReplie['type']) ? $quickReplie['type'] : 'text';

        $model->setContentType($type);

        if (!empty($quickReplie['title'])) {
            $model->setPayload($quickReplie['payload']);
        }

        if (!empty($quickReplie['title'])) {
            $model->setTitle($quickReplie['title']);
        }

        if (!empty($quickReplie['image'])) {
            $model->setImageUrl($quickReplie['image']);
        }

        $this->quickReplies[] = $model;
    }

    /**
     * Set quick replies
     *
     * @return array Quick replies
     */
    public function getQuickReplies()
    {
        return $this->quickReplies;
    }

    /**
     * Populate text
     *
     * @param string $text Text
     *
     * @return Message
     */
    public function text($text)
    {
        $this->setText($text);

        return $this;
    }

    /**
     * Populate audio
     *
     * @param string|int $resource Url or attachment id
     * @param bool $isReusable Is reusable
     *
     * @return Message
     */
    public function audio($resource, $isReusable = false)
    {
        $payload = [
            'is_reusable' => $isReusable
        ];

        if (is_int($resource)) {
            $payload['attachment_id'] = $resource;
        } else {
            $payload['url'] = $resource;
        }

        $this->setAttachment('audio', $payload);

        return $this;
    }

    /**
     * Populate video
     *
     * @param string|int $resource Url or attachment id
     * @param bool $isReusable Is reusable
     *
     * @return Message
     */
    public function video($resource, $isReusable = false)
    {
        $payload = [
            'is_reusable' => $isReusable
        ];

        if (is_int($resource)) {
            $payload['attachment_id'] = $resource;
        } else {
            $payload['url'] = $resource;
        }

        $this->setAttachment('video', $payload);

        return $this;
    }

    /**
     * Populate image
     *
     * @param string|int $resource Url or attachment id
     * @param bool $isReusable Is reusable
     *
     * @return Message
     */
    public function image($resource, $isReusable = false)
    {
        $payload = [
            'is_reusable' => $isReusable
        ];

        if (is_int($resource)) {
            $payload['attachment_id'] = $resource;
        } else {
            $payload['url'] = $resource;
        }

        $this->setAttachment('image', $payload);

        return $this;
    }

    /**
     * Populate file
     *
     * @param string|int $resource Url or attachment id
     * @param bool $isReusable Is reusable
     *
     * @return Message
     */
    public function file($resource, $isReusable = false)
    {
        $payload = [
            'is_reusable' => $isReusable
        ];

        if (is_int($resource)) {
            $payload['attachment_id'] = $resource;
        } else {
            $payload['url'] = $resource;
        }

        $this->setAttachment('file', $payload);

        return $this;
    }

    /**
     * Populate call
     *
     * @param string $text Text
     * @param string $title Title
     * @param array $payload Payload
     *
     * @return Message
     */
    public function call($text, $title, $payload)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => [
                [
                    'type' => 'phone_number',
                    'title' => $title,
                    'payload' => $payload,
                ]
            ],
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate simple url button
     *
     * @param string $text Text
     * @param string $title Title
     * @param string $url Url
     *
     * @return Message
     */
    public function url($text, $title, $url)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => [
                [
                    'title' => $title,
                    'url' => $url
                ]
            ],
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate simple postback button
     *
     * @param string $text Text
     * @param string $title Title
     * @param string $postback Postback
     *
     * @return Message
     */
    public function postback($text, $title, $postback)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => [
                [
                    'type' => 'postback',
                    'title' => $title,
                    'payload' => $postback
                ]
            ],
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate login button
     *
     * @param string $text Text
     * @param string $url Url
     *
     * @return Message
     */
    public function login($text, $url)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => [
                [
                    'type' => 'account_link',
                    'url' => $url
                ]
            ],
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate logout button
     *
     * @param string $text Text
     *
     * @return Message
     */
    public function logout($text)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => [
                [
                    'type' => 'account_unlink'
                ]
            ],
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate button
     *
     * @param string $text Text
     * @param array $buttons Buttons
     *
     * @return Message
     */
    public function button($text, $buttons)
    {
        $payload = [
            'template_type' => 'button',
            'text' => $text,
            'buttons' => $buttons,
        ];

        $this->setAttachment('template', $payload);

        return $this;
    }

    /**
     * Populate quick replies
     *
     * @param string $text Text
     * @param array $quickReplies Quick replies
     *
     * @return Message
     */
    public function quickReplies($text, $quickReplies)
    {
        $this->setText($text);

        foreach ($quickReplies as $quickReplie) {
            $this->setQuickReplies($quickReplie);
        }

        return $this;
    }

    /**
     * Populate qucik replies text
     *
     * @param string $text Text
     * @param string $title Title
     * @param string $postback Postback
     *
     * @return Message
     */
    public function quickReplie($text, $title, $postback, $image = null)
    {
        $this->setText($text);

        $payload = [
            'title' => $title,
            'payload' => $postback,
        ];

        if (!empty($image)) {
            $payload['image'] = $image;
        }

        $this->setQuickReplies($payload);

        return $this;
    }

    /**
     * Populate qucik replies location
     *
     * @param string $text Text
     *
     * @return Message
     */
    public function location($text)
    {
        $this->setText($text);

        $this->setQuickReplies([
            'type' => 'location'
        ]);

        return $this;
    }

    /**
     * Populate qucik replies phone number
     *
     * @param string $text Text
     *
     * @return Message
     */
    public function phoneNumber($text)
    {
        $this->setText($text);

        $this->setQuickReplies([
            'type' => 'user_phone_number'
        ]);

        return $this;
    }

    /**
     * Populate qucik replies email
     *
     * @param string $text Text
     *
     * @return Message
     */
    public function email($text)
    {
        $this->setText($text);

        $this->setQuickReplies([
            'type' => 'user_email'
        ]);

        return $this;
    }
}


<?php

namespace FbMessengerBot;

/**
 * Class Messenger
 *
 */
class Payload
{
    /**
     * @var string
     */
    protected $url;

    /**
     * @var bool
     */
    protected $isReusable;

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

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

    /**
     * @var array
     */
    protected $buttons;

    /**
     * @var int
     */
    protected $attachmentId;

    /**
     * Set url
     *
     * @param string $url Url
     */
    public function setUrl($url)
    {
        $this->url = $url;
    }

    /**
     * Get url
     *
     * @return string Url
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Set is reusable
     *
     * @param bool $isReusable Is reusable
     */
    public function setIsReusable($isReusable)
    {
        $this->isReusable = $isReusable;
    }

    /**
     * Get is reusable
     *
     * @return bool Is reusable
     */
    public function getIsReusable()
    {
        return $this->isReusable;
    }

    /**
     * Set template type
     *
     * @param string $templateType Template type
     */
    public function setTemplateType($templateType)
    {
        $this->templateType = $templateType;
    }

    /**
     * Get template type
     *
     * @return string Template type
     */
    public function getTemplateType()
    {
        return $this->templateType;
    }

    /**
     * Set text
     *
     * @param string $text Text
     */
    public function setText($text)
    {
        $this->text = $text;
    }

    /**
     * Get text
     *
     * @return string Text
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set attachment id
     *
     * @param int $attachmentId Attachment id
     */
    public function setAttachmentId($attachmentId)
    {
        $this->attachmentId = $attachmentId;
    }

    /**
     * Get attachment id
     *
     * @return int Attachment id
     */
    public function getAttachmentId()
    {
        return $this->attachmentId;
    }

    /**
     * Set button
     *
     * @param array $payload Payload
     */
    public function setButton($payload)
    {
        $model = new Button();

        $model->setType(!empty($payload['type']) ? $payload['type'] : 'web_url');

        if (!empty($payload['title'])) {
            $model->setTitle($payload['title']);
        }

        if (!empty($payload['payload'])) {
            $model->setPayload($payload['payload']);
        }

        if (!empty($payload['url'])) {
            $model->setUrl($payload['url']);
        }

        $this->buttons[] = $model;
    }

    /**
     * Get button
     *
     * @return array Button
     */
    public function getButton()
    {
        return $this->buttons;
    }
}


<?php

namespace FbMessengerBot;

/**
 * Class Body
 *
 */
class Body
{
    /**
     * @var Recipient
     */
    protected $recipient;

    /**
     * @var Message
     */
    protected $message;

    /**
     * @var Sender action
     */
    protected $senderAction;

    /**
     * Set recipient
     *
     * @param int $recipientId Recipient id
     */
    public function setRecipient($recipientId)
    {
        $recipient = new Recipient();

        $recipient->setId($recipientId);

        $this->recipient = $recipient;
    }

    /**
     * Get recipient
     *
     * @return Recipient
     */
    public function getRecipient()
    {
        return $this->recipient;
    }

    /**
     * Set message
     *
     * @param Message $message
     */
    public function setMessage($message)
    {
        $this->message = $message;
    }

    /**
     * Get message
     *
     * @return Message
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * Set sender action
     *
     * @param string $senderAction
     */
    public function setSenderAction($senderAction)
    {
        $this->senderAction = $senderAction;
    }

    /**
     * Get sender action
     *
     * @return Sender action
     */
    public function getSenderAction()
    {
        return $this->senderAction;
    }
}


<?php

namespace FbMessengerBot;

/**
 * Class Messenger
 *
 */
class Messenger
{
    /**
     * Post request type
     */
    const TYPE_POST = 'POST';

    /**
     * Get request type
     */
    const TYPE_GET = 'GET';

    /**
     *
     * @var string
     */
    protected $url = 'https://graph.facebook.com/v2.11/';

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

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

    /**
     * @var Body
     */
    protected $body;

    /**
     * @var array
     */
    protected $services = [];

    /**
     * Messenger constructor
     *
     * @param array $config Config
     */
    public function __construct($config)
    {
        $this->setAccessToken($config['access_token']);

        if (!empty($config['verify_token'])) {
            $this->setVerifyToken($config['verify_token']);
        }
    }

    /**
     * Set access token
     *
     * @param string $accessToken Access token
     */
    public function setAccessToken($accessToken)
    {
        $this->accessToken = $accessToken;
    }

    /**
     * Get access token
     *
     * @return string Access token
     */
    public function getAccessToken()
    {
        return $this->accessToken;
    }

    /**
     * Set verify token
     *
     * @param string $verifyToken Verify token
     */
    public function setVerifyToken($verifyToken)
    {
        $this->verifyToken = $verifyToken;
    }

    /**
     * Get verify token
     *
     * @return string Verify token
     */
    public function getVerifyToken()
    {
        return $this->verifyToken;
    }

    /**
     * Set body
     *
     * @param Body $body
     */
    public function setBody($body)
    {
        $this->body = $body;
    }

    /**
     * Get body
     *
     * @return Body
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * Send message
     *
     * @param int $recipientId Recipient id
     * @param Message $message
     *
     * @return array
     */
    public function send($recipientId, Message $message)
    {
        $body = new Body;

        $body->setRecipient($recipientId);
        $body->setMessage($message);

        return $this->sendApi($body);
    }

    /**
     * Sender action
     *
     * @param int $recipientId Recipient id
     * @param string $type
     *
     * @return array
     */
    public function senderAction($recipientId, $type)
    {
        $body = new Body;

        $body->setRecipient($recipientId);
        $body->setSenderAction($type);

        return $this->sendApi($body);
    }

    /**
     * Send api
     *
     * @param Boyd $body
     *
     * @return array
     */
    public function sendApi(Body $body)
    {
        $this->setBody($body);

        $helper = new Helper;
        $body = $helper->objectToArray($body);

        return $this->api('me/messages', $body);
    }

    /**
     * Request to Facebook API
     *
     * @param string $url Url
     * @param array  $body Body
     * @param string $type Request type (POST)
     *
     * @return array
     */
    public function api($url, $body = null, $type = self::TYPE_POST)
    {
        $body['access_token'] = $this->accessToken;

        $this->setBody($body);

        $headers = [
            'Content-Type: application/json',
        ];

        if ($type == self::TYPE_GET) {
            $url .= '?'.http_build_query($body);
        }

        $curl = curl_init($this->url . $url);

        if($type == self::TYPE_POST) {
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($body));
        }

        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($curl);
        curl_close($curl);

        return json_decode($response, true);
    }

    /**
     * Listen message
     *
     * @return array
     */
    public function listen()
    {
        if (!empty($_REQUEST['hub_verify_token']) && $_REQUEST['hub_verify_token'] === $this->verifyToken) {
            echo $_REQUEST['hub_challenge'];
            exit;
        }

        return json_decode(file_get_contents('php://input'), true);
    }

    /**
     * Populate service
     *
     * @param string $name Service name
     *
     * @return object
     */
    public function __get($name)
    {
        if (!in_array($name, ['message'])) {
            $trace = debug_backtrace();

            throw new \Exception(sprintf('Undefined property via __get(): %s in %s on line %u', $name, $trace[0]['file'], $trace[0]['line']));
        }

        if (isset($this->services[$name])) {
            return $this->services[$name];
        }

        $serviceName = sprintf('%s\\%s', __NAMESPACE__, ucfirst($name));

        $this->services[$name] = new $serviceName($this);

        return $this->services[$name];
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * HydraBundle Configuration
 *
 * Definition of the configuration settings available for the HydraBundle;
 * responsible for how the different settings are normalized, validated,
 * and merged.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('hydra');

// TODO Add API title, description, entrypoint, and global status code descriptions,
// perhaps also references to supported classes

        $rootNode
            ->fixXmlConfig('mapping')
            ->children()
                ->scalarNode('auto_mapping')->defaultValue(true)->end()
                ->scalarNode('naming_strategy')->defaultValue('hydra.naming_strategy.default')->end()
// FIXXME Do we need this!?
                // ->scalarNode('metadata_factory_class')
                //     ->defaultValue('ML\Hydra\Metadata\MetadataFactory')
                // ->end()
                ->arrayNode('metadata_cache_driver')
                    ->addDefaultsIfNotSet()
                    ->beforeNormalization()
                        ->ifString()
                        ->then(function($v) { return array('type' => $v); })
                    ->end()
                    ->children()
                        ->scalarNode('type')->defaultValue('array')->end()
                        ->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/hydra')->end()
                        ->scalarNode('host')->end()
                        ->scalarNode('port')->end()
                        ->scalarNode('instance_class')->end()
                        ->scalarNode('class')->end()
                        ->scalarNode('id')->end()
                    ->end()
                ->end()
                ->arrayNode('mappings')
                    ->useAttributeAsKey('name')
                    ->prototype('array')
                        ->beforeNormalization()
                            ->ifString()
                            ->then(function($v) { return array('type' => $v); })
                        ->end()
                        ->treatNullLike(array())
                        ->treatFalseLike(array('mapping' => false))
                        ->performNoDeepMerging()
                        ->children()
                            ->scalarNode('mapping')->defaultValue(true)->end()
                            ->scalarNode('type')->end()
                            ->scalarNode('dir')->end()
                            ->scalarNode('prefix')->end()
                            ->booleanNode('is_bundle')->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace ML\HydraBundle\EventListener;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use ML\HydraBundle\HydraApi;

/**
 * The ResponseListener adds an HTTP Link header pointing to the Hydra
 * ApiDocumentation.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class ResponseListener
{
    /**
     * @var ML\HydraBundle\HydraApi
     */
    protected $hydra;

    /**
     * Constructor
     *
     * @param HydraApi $hydraApi The Hydra API.
     */
    public function __construct(HydraApi $hydraApi)
    {
        $this->hydra = $hydraApi;
    }

    /**
     * Marks request that whose controller return value should be serialized
     * by the Hydra serializer
     *
     * @param FilterControllerEvent $event A FilterControllerEvent instance
     */
    public function onKernelResponse(FilterResponseEvent $event)
    {
        $response = $event->getResponse();

        $response->headers->set('Access-Control-Allow-Origin', '*');
        $response->headers->set('Access-Control-Allow-Credentials', 'true');
        $response->headers->set('Access-Control-Expose-Headers', 'Link');
        $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');

        $response->headers->set(
            'Link',
            '<' . $this->hydra->getDocumentationUrl() . '>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'
        );
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\DatatypeNormalizer;

use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use ML\JsonLD\TypedValue;

/**
 * Converts XSD dateTime values into PHP DateTime objects and vice-versa
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
{
    /**
     * The supported format
     */
    const FORMAT = 'jsonld';

    /**
     * The XML Schema dateTime datatype IRI
     */
    const XSD_DATETIME_IRI = 'http://www.w3.org/2001/XMLSchema#dateTime';

    /**
     * The canonical XSD dateTime format (all times have to be in UTC)
     */
    const XSD_DATETIME_FORMAT = 'Y-m-d\TH:i:s\Z';

    /**
     * Returns the IRI identifying the (data)type
     *
     * @return string The IRI identifying the (data)type
     */
    public function getTypeIri()
    {
        return self::XSD_DATETIME_IRI;
    }

    /**
     * Normalizes an object into a set of arrays/scalars
     *
     * @param object $object object to normalize
     * @param string $format format the normalization result will be encoded as
     * @param array  $context Context options for the normalizer
     *
     * @return string
     */
    public function normalize($object, $format = null, array $context = array())
    {
        $dt = clone $object;
        $dt->setTimezone(new \DateTimeZone('UTC'));

        return $object->format(self::XSD_DATETIME_FORMAT);
    }

    /**
     * Checks whether the given class is supported for normalization by this normalizer
     *
     * @param mixed  $data   Data to normalize.
     * @param string $format The format being (de-)serialized from or into.
     *
     * @return Boolean
     */
    public function supportsNormalization($data, $format = null)
    {
        return is_object($data) && ($data instanceof \DateTime) && (self::FORMAT === $format);
    }

    /**
     * Denormalizes data back into an object of the given class
     *
     * @param mixed  $data   data to restore
     * @param string $class  the expected class to instantiate
     * @param string $format format the given data was extracted from
     * @param array  $context options available to the denormalizer
     *
     * @return DateTime
     *
     * @throws RuntimeException If the data can't be denormalized
     */
    public function denormalize($data, $class, $format = null, array $context = array())
    {
        $value = $data;

        if (is_array($data)) {
            if (!isset($data['@value']) || !isset($data['@type'])) {
                throw new RuntimeException(
                    "Cannot denormalize the data as it isn't a valid JSON-LD typed value: " .
                    var_export($data, true)
                );
            }

            if (self::XSD_DATETIME_IRI !== $data['@type']) {
                throw new RuntimeException(
                    "Cannot denormalize the data as it isn't a XSD dateTime value: " .
                    var_export($data, true)
                );
            }

            $value = $data['@value'];
        } elseif (!is_string($data)) {
            throw new RuntimeException(
                "Cannot denormalize the data into a DateTime object: " .
                var_export($data, true)
            );
        }

        try {
            $date = new \DateTime($value);

            return $date;
        } catch(Exception $e) {
            throw new RuntimeException(
                "Cannot denormalize the data as the value is invalid: " . var_export($data, true),
                0,
                $e
            );
        }
    }

    /**
     * Checks whether the given class is supported for denormalization by this normalizer
     *
     * @param mixed  $data   Data to denormalize from.
     * @param string $type   The class to which the data should be denormalized.
     * @param string $format The format being deserialized from.
     *
     * @return Boolean
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        // TODO Check data?
        return ('DateTime' === $type) && (self::FORMAT === $format);
    }
}


<?php

namespace ML\HydraBundle\Entity;

use ML\HydraBundle\Mapping as Hydra;

/**
 * A generic collection keeping references to a number of resources.
 *
 * @Hydra\Expose(iri="http://www.w3.org/ns/hydra/core#Collection")
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class Collection
{
    /**
     * @var string The identifier of this collection.
     *
     * @Hydra\Expose(as = "@id")
     */
    private $id;

    /**
     * @var array The members of this collection.
     *
     * @Hydra\Expose(iri="http://www.w3.org/ns/hydra/core#member")
     */
    private $members;

    /**
     * Constructor
     *
     * @var string $id      The IRI identifying this Collection.
     * @var array  $members The members.
     */
    public function __construct($id, $members)
    {
        $this->id = $id;
        $this->setMembers($members);
    }

    /**
     * Set Id
     *
     * @param string $id The IRI identifying this Collection.
     * @return Collection
     */
    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

    /**
     * Get Id
     *
     * @return string The IRI identifying this Collection.
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set members
     *
     * @param array $members
     * @return Collection
     */
    public function setMembers($members)
    {
        if (!is_array($members) && !($members instanceof \ArrayAccess) && !($members instanceof \Traversable)) {
            // TODO Improve this
            throw new \Exception("The members of a Collection must be an array or an object implementing ArrayAccess.");
        }

        $this->members = $members;

        return $this;
    }

    /**
     * Get members
     *
     * @return array
     */
    public function getMembers()
    {
        return $this->members;
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace ML\HydraBundle\EventListener;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;
use Doctrine\Common\Util\ClassUtils;
use ML\HydraBundle\JsonLdResponse;
use ML\HydraBundle\HydraApi;
use ML\HydraBundle\Entity\Collection;

/**
 * The SerializerListener class makes sure that the data returned by a
 * controller is serialized as JSON-LD.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class SerializerListener
{
    /**
     * @var ML\HydraBundle\HydraApi
     */
    protected $hydra;

    /**
     * @var Symfony\Component\Serializer\SerializerInterface
     */
    protected $serializer;

    /**
     * Constructor
     *
     * @param HydraApi            $hydraApi    The Hydra API.
     * @param SerializerInterface $serializer The serializer.
     */
    public function __construct(HydraApi $hydraApi, SerializerInterface $serializer, $annotationReader)
    {
        $this->hydraApi = $hydraApi;
        $this->serializer = $serializer;

        // FIXME The annotation reader is only necessary for the isHydraOperation() method which should be moved elsewhere
        $this->annotationReader = $annotationReader;
    }

    /**
     * Marks request that whose controller return value should be serialized
     * by the Hydra serializer
     *
     * @param FilterControllerEvent $event A FilterControllerEvent instance
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        if (!is_array($controller = $event->getController())) {
            return;
        }

        $request = $event->getRequest();

        $method = new \ReflectionMethod($controller[0], $controller[1]);

        if ($this->isHydraOperation($method)) {
            $request->attributes->set('__hydra_serialize', true);

            // TODO Add support for serialization groups and other serialization formats
        }
    }

    /**
     * Renders the template and initializes a new response object with the
     * rendered template content.
     *
     * @param GetResponseForControllerResultEvent $event A GetResponseForControllerResultEvent instance
     */
    public function onKernelView(GetResponseForControllerResultEvent $event)
    {
        $request = $event->getRequest();
        $result = $event->getControllerResult();

        if (!$request->attributes->get('__hydra_serialize')) {
            return;
        }

        if (is_array($result) || ($result instanceof \ArrayAccess) || ($result instanceof \Traversable)) {
            $result = new Collection($request->getUri(), $result);
        } elseif (null === $result) {
            $event->setResponse(new JsonLdResponse('', 200));
            return;
        } elseif (!is_object($result)) {
            throw new \Exception("A Hydra controller must return either an array or an object, got a(n) " . gettype($result));
        }

        $serialized = $this->serializer->serialize($result, 'jsonld');

        $event->setResponse(new JsonLdResponse($serialized));
    }

    /**
     * Does the specified method represent a Hydra Operation?
     *
     * This information is used to determine whether it's return value
     * should be serialized by the Hydra serializer.
     *
     * Currently annotations are the only way to specify a method to be a
     * Hydra operation.
     *
     * @param \ReflectionMethod $method The controller method.
     *
     * @return boolean True if the method represents a Hydra Operation,
     *                 false otherwise
     */
    private function isHydraOperation(\ReflectionMethod $method)
    {
        $annotation = $this->annotationReader->getMethodAnnotation(
            $method,
            'ML\HydraBundle\Mapping\Operation'
        );

        return null !== $annotation;
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\Generator;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use Sensio\Bundle\GeneratorBundle\Generator\DoctrineCrudGenerator as SensioDoctrineCrudGenerator;
use Doctrine\ORM\Mapping\ClassMetadataInfo;

/**
 * Generates a CRUD controller.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class DoctrineCrudGenerator extends SensioDoctrineCrudGenerator
{
    /**
     * Generate the CRUD controller.
     *
     * @param BundleInterface   $bundle           A bundle object
     * @param string            $entity           The entity relative class name
     * @param ClassMetadataInfo $metadata         The entity class metadata
     * @param string            $format           The configuration format (currently just annotation)
     * @param string            $routePrefix      The route name prefix
     * @param array             $needWriteActions Wether or not to generate write actions
     *
     * @throws \RuntimeException
     */
    public function generate(BundleInterface $bundle, $entity, ClassMetadataInfo $metadata, $format, $routePrefix, $needWriteActions, $forceOverwrite)
    {
        // Remove trailing "/"
        if ('/' === $routePrefix[strlen($routePrefix) - 1]) {
            $routePrefix = substr($routePrefix, 0, -1);
        }

        $this->routePrefix = $routePrefix;

        // Convert camel-cased class name to underscores
        $this->routeNamePrefix = strtolower(preg_replace_callback(
            '/([a-z0-9])([A-Z])/',
            function ($match) {
                return $match[1] . '_' . strtolower($match[2]);
            },
            substr($entity, strrpos($entity, '\\'))
        ));

        $this->actions = $needWriteActions
            ? array('collection_get', 'collection_post', 'entity_get', 'entity_put', 'entity_delete')
            : array('collection_get', 'entity_get');

        if (count($metadata->identifier) > 1) {
            throw new \RuntimeException('The CRUD generator does not support entity classes with multiple primary keys.');
        }

        if (!in_array('id', $metadata->identifier)) {
            throw new \RuntimeException('The CRUD generator expects the entity object has a primary key field named "id" with a getId() method.');
        }

        $this->entity   = $entity;
        $this->bundle   = $bundle;
        $this->metadata = $metadata;
        $this->format   = 'annotation';

        $this->generateControllerClass($forceOverwrite);
        $this->generateTestClass();
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\Mapping\Driver;

use Reflector;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use ML\HydraBundle\Mapping\ClassMetadata;
use ML\HydraBundle\Mapping\PropertyDefinition;
use ML\HydraBundle\Mapping\OperationDefinition;
use Symfony\Component\Routing\RouterInterface;
use Doctrine\Common\Annotations\Reader;

/**
 * The Hydra AnnotationDriver
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class AnnotationDriver implements MappingDriver
{
    /**
     * The paths where to look for mapping files.
     *
     * @var array
     */
    protected $paths = array();

    /**
     * The file extension of mapping documents.
     *
     * @var string
     */
    protected $fileExtension = '.php';

    /**
     * Cache for AnnotationDriver#getAllClassNames()
     *
     * @var array
     */
    protected $classNames;

    /**
     * @var array<OperationDefintion> Already generated route metadata
     */
    private $routeMetadata = array();

    /**
     * Constructor
     *
     * Initializes a new AnnotationDriver that uses the given annotation
     * reader for reading docblock annotations.
     *
     * @param Reader $reader The annotation reader to use.
     * @param string|array $paths One or multiple paths where mapping
     *                            classes can be found.
     */
    public function __construct(Reader $reader, $paths = null, RouterInterface $router = null)
    {
        $this->reader = $reader;
        if ($paths) {
            $this->addPaths((array) $paths);
        }

        // FIXXME This member variable doesn't exist, we shouldn't inject the router interface directly
        $this->router = $router;
    }

    /**
     * Append lookup paths to metadata driver.
     *
     * @param array $paths
     */
    public function addPaths(array $paths)
    {
        $this->paths = array_unique(array_merge($this->paths, $paths));
    }

    /**
     * Retrieve the defined metadata lookup paths.
     *
     * @return array
     */
    public function getPaths()
    {
        return $this->paths;
    }

    /**
     * Retrieve the current annotation reader
     *
     * @return AnnotationReader
     */
    public function getReader()
    {
        return $this->reader;
    }

    /**
     * Set the file extension used to look for mapping files under
     *
     * @param string $fileExtension The file extension to set
     * @return void
     */
    public function setFileExtension($fileExtension)
    {
        $this->fileExtension = $fileExtension;
    }

    /**
     * Get the file extension used to look for mapping files under
     *
     * @return string
     */
    public function getFileExtension()
    {
        return $this->fileExtension;
    }

    /**
     * {@inheritDoc}
     */
    public function getAllClassNames()
    {
        if ($this->classNames !== null) {
            return $this->classNames;
        }

        if ( ! $this->paths) {
            throw MappingException::pathRequired();
        }

        $classes = array();
        $includedFiles = array();

        foreach ($this->paths as $path) {
            if ( ! is_dir($path)) {
                throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
            }

            $iterator = new \RegexIterator(
                new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
                    \RecursiveIteratorIterator::LEAVES_ONLY
                ),
                '/^.+' . preg_quote($this->fileExtension) . '$/i',
                \RecursiveRegexIterator::GET_MATCH
            );

            foreach ($iterator as $file) {
                $sourceFile = realpath($file[0]);

                require_once $sourceFile;

                $includedFiles[] = $sourceFile;
            }
        }

        $declared = get_declared_classes();

        foreach ($declared as $className) {
            $rc = new ReflectionClass($className);
            $sourceFile = $rc->getFileName();
            if (in_array($sourceFile, $includedFiles) && $this->isExposed($className)) {
                $classes[] = $className;
            }
        }

        $this->classNames = $classes;

        return $classes;
    }

    /**
     * {@inheritDoc}
     */
    public function isExposed($className)
    {
        $annotation = $this->reader->getClassAnnotation(
            new ReflectionClass($className),
            'ML\HydraBundle\Mapping\Expose'
        );

        return null !== $annotation;
    }

    /**
     * {@inheritDoc}
     */
    public function loadMetadataForClass($className)
    {
        $class = new ReflectionClass($className);
        $classAnnotations = $this->reader->getClassAnnotations($class);

        if ($classAnnotations) {
            foreach ($classAnnotations as $key => $annot) {
                if ( ! is_numeric($key)) {
                    continue;
                }

                $classAnnotations[get_class($annot)] = $annot;
            }
        }

        if (!isset($classAnnotations['ML\HydraBundle\Mapping\Expose'])) {
            return null;
        }

        $annotation = $classAnnotations['ML\HydraBundle\Mapping\Expose'];

        $metadata = new ClassMetadata($className);
        $metadata->setExposeAs($annotation->as);
        $metadata->setIri($annotation->getIri());

        $this->documentRouteAndOperations($metadata, $class);
        $this->documentProperties($metadata, $class);

        // If this class represents an external definition, we don't need to
        // collect all data; just the one necessary for the serializer
        if ($metadata->isExternalReference()) {
            return $metadata;
        }

        $docu = $this->getDocBlockText($class);

        $metadata->setTitle($docu['title']);
        $metadata->setDescription($docu['description']);

        return $metadata;
    }

    /**
     * Document the route and operations associated to an element
     *
     * @param Reflector $element The element being processed.
     */
    private function documentRouteAndOperations($metadata, Reflector $element)
    {
        if ((null !== ($annotation = $this->getAnnotation($element, 'ML\HydraBundle\Mapping\Id'))) ||
            (null !== ($annotation = $this->getAnnotation($element, 'ML\HydraBundle\Mapping\Route')))) {
            // TODO Check that the IRI template can be filled!?
            $metadata->setRoute($this->getRouteMetadata($annotation->route));
        }

        $annotation = $this->getAnnotation($element, 'ML\HydraBundle\Mapping\Operations');
        if (null !== $annotation) {
            $operations = array_unique($annotation->operations);

            $operationsMetadata = array_map(array($this, 'getRouteMetadata'), $operations);

            $metadata->setOperations($operationsMetadata);
        }

        if (null !== ($route = $metadata->getRoute())) {
            // Add the route to the supported operations
            $metadata->addOperation($this->getRouteMetadata($route->getName()));
        } elseif (null !== $annotation) {
            // ... or use an operation as route if none is set

            // FIXXME: Do this only for GET operations!
            $metadata->setRoute($this->getRouteMetadata(reset($annotation->operations)));
        }

        if (($metadata instanceof PropertyDefinition) && (count($operations = $metadata->getOperations()) > 0)) {
            foreach ($operations as $operation) {
                if (('GET' === $operation->getMethod()) && (null !== $operation->getReturns())) {
                    $metadata->setType($operation->getReturns());
                    return;
                }
            }

            $metadata->setType('ML\HydraBundle\Entity\Resource');
        }
    }

    /**
     * Get information about a route
     *
     * @param string $routeName
     */
    protected function getRouteMetadata($routeName)
    {
        if (isset($this->routeMetadata[$routeName])) {
            return $this->routeMetadata[$routeName];
        }

        $route = $this->router->getRouteCollection()->get($routeName);

        if (null === $route) {
            // TODO Improve this
            throw new \Exception(sprintf('The route "%s" couldn\'t be found', $routeName));
        }

        // TODO Check this
        $controller = $route->getDefault('_controller');
        if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) {
            $class = $matches[1];
            $method = $matches[2];
        } elseif (preg_match('#(.+):([\w]+)#', $controller, $matches)) {
            $controller = $matches[1];
            $method = $matches[2];
            if ($this->container->has($controller)) {
                $this->container->enterScope('request');
                $this->container->set('request', new Request());
                $class = get_class($this->container->get($controller));
                $this->container->leaveScope('request');
            }
        }

        if (isset($class) && isset($method)) {
            try {
                $method = new ReflectionMethod($class, $method);
            } catch (\ReflectionException $e) {
                throw new \Exception(sprintf('The controller method for the route "%s" couldn\'t be found', $routeName));
            }
        }

        if (null === ($annotation = $this->getAnnotation($method, 'ML\HydraBundle\Mapping\Operation'))) {
            throw new \Exception(sprintf('The controller method for the route "%s" is not marked as an operation', $routeName));
        }

        $operation = new OperationDefinition($routeName);
        $operation->setIri($annotation->getIri());
        $operation->setType($annotation->type);

        $operation->setRoute($route);

        $tmp = $this->getDocBlockText($method);

        $operation->setTitle($tmp['title']);
        $operation->setDescription($tmp['description']);

        $operation->setExpects($annotation->expect);
        $operation->setStatusCodes($annotation->status_codes);

        // TODO Check this
        $tmp = $this->getType($method);
        $operation->setReturns($tmp['type']);

        // TODO Check this! Should we use the return type instead?
        if ($tmp['is_array'] || (null !== ($annotation = $this->getAnnotation($method, 'ML\HydraBundle\Mapping\Collection')))) {
            $operation->setReturns('ML\HydraBundle\Entity\Collection');
        }

        // if (('array' === $operation['return']['type']) || (self::HYDRA_COLLECTION === $operation['return']['type'])) {
        //     $operation['return']['type'] = self::HYDRA_COLLECTION;
        // }

        // method/IRI
        $operation->setMethod($route->getRequirement('_method'));
        // $operation['path'] = $route->getPath();
        // $operation['variables'] = $route->compile()->getVariables();
        // $operation['defaults'] = $route->getDefaults();
        // unset($operation['defaults']['_controller']);

        // Cache the metadata since it might be needed several times
        $this->routeMetadata[$routeName] = $operation;

        return $operation;
    }

    /**
     * Document the properties and methods associated to a class
     *
     * @param ClassMetadata $metadata The class definition
     * @param ReflectionClass $class    The class whose properties and
     *                                  methods should be documented.
     */
    private function documentProperties(ClassMetadata $metadata, ReflectionClass $class)
    {
/*

        $interfaces = $class->getInterfaces();
        $linkRelationMethods = array();
        foreach ($interfaces as $interface) {
            if (null !== $this->getAnnotation($interface, $linkRelationAnnot)) {
                if (false === isset($documentation['rels'][$interface->name])) {
                    $documentation['rels'][$interface->name] = array();
                    foreach ($interface->getMethods() as $method) {
                        if ($method->isPublic()) {
                            $documentation['rels'][$interface->name][$method->name] = $interface->name;
                        }
                    }
                }

                $linkRelationMethods += $documentation['rels'][$interface->name];
            }
        }
*/

        $properties = array();
        $elements = array_merge($class->getProperties(), $class->getMethods());

        foreach ($elements as $element) {
            $annotation = $this->getAnnotation($element, 'ML\HydraBundle\Mapping\Expose');

            if (null === $annotation) {
                continue;
            }

            // $exposeAs = $element->name;
            // if ($annotation->as) {
            //     $exposeAs = $annotation->as;

            //     if ($annotation->getIri()) {
            //         $property['iri'] = $annotation->getIri();
            //     } else {
            //         $property['iri'] = $exposeClassAs . '/' . $exposeAs;
            //     }
            // } else {
            //     $exposeAs = $this->propertirize($exposeAs);

            //     if ($annotation->getIri()) {
            //         $property['iri'] = $annotation->getIri();
            //     } else {
            //         $property['iri'] = $this->camelize($exposeAs);
            //         $property['iri'][0] = strtolower($property['iri'][0]);
            //         $property['iri'] =  $exposeClassAs . '/' . $property['iri'];
            //     }
            // }

            $property = new PropertyDefinition($class->name, $element->name);
            $property->setExposeAs($annotation->as);
            $property->setIri($annotation->getIri());

            if (null !== $annotation->required) {
                $property->setRequired($annotation->required);
            }
            if (null !== $annotation->readonly) {
                $property->setReadOnly($annotation->readonly);
            }
            if (null !== $annotation->writeonly) {
                $property->setWriteOnly($annotation->writeonly);
            }

            $tmp = $this->getDocBlockText($element);
            $property->setTitle($tmp['title']);
            $property->setDescription($tmp['description']);

            $tmp = $this->getType($element);
            $property->setType($tmp['type']);

            $this->documentRouteAndOperations($property, $element);

            if (null !== ($annotation = $this->getAnnotation($element, 'ML\HydraBundle\Mapping\Collection'))) {
                // TODO Check for conflicting routes!?
                // TODO Check that the IRI template can be filled!?
                $property->setRoute($this->getRouteMetadata($annotation->route));

                if (false === $property->supportsOperation($annotation->route)) {
                    $property->addOperation($this->getRouteMetadata($annotation->route));
                }

                $property->setType('ML\HydraBundle\Entity\Collection');
                $property->setReadOnly(true);
            }

/*
            if ($element instanceof ReflectionMethod) {
                if (array_key_exists($element->name, $linkRelationMethods)) {
                    $property['original_type'] .= ' --- ' . $linkRelationMethods[$element->name] . '::' . $element->name;
                }
            }
*/
            // TODO Validate definition, this here isn't the right place to do so, create a metadata factory

            $properties[] = $property;
        }

        // $documentation['class2type'][$class->name] = $exposeClassAs;
        // $documentation['types'][$exposeClassAs] = $result;

        $metadata->setProperties($properties);
    }

    /**
     * Get the specified annotation of an element
     *
     * @param  Reflector $element    The element whose annotation should be
     *                               retrieved
     * @param  string    $annotation The class of the annotation to retrieve
     *
     * @return object|null The annotation or null if the element doesn't
     *                     have the specified annotation
     */
    private function getAnnotation(Reflector $element, $annotation)
    {
        if ($element instanceof ReflectionClass) {
            return $this->reader->getClassAnnotation($element, $annotation);
        } elseif ($element instanceof ReflectionMethod) {
            return $this->reader->getMethodAnnotation($element, $annotation);
        } elseif ($element instanceof ReflectionProperty) {
            return $this->reader->getPropertyAnnotation($element, $annotation);
        }

        return null;
    }

    protected function getDocBlockText(Reflector $element)
    {
        $text = $element->getDocComment();

        // store @var annotations if available (just one line supported)
        preg_match('/@var\s+([^\s]+)((?:[ ]+)[^\r\n]*)?/', $text, $var);

        // then remove annotations
        $text = preg_replace('/^\s+\* @[\w0-9]+.*/msi', '', $text);

        // let's clean the doc block
        $text = str_replace('/**', '', $text);
        $text = str_replace('*/', '', $text);
        $text = str_replace('*', '', $text);
        $text = str_replace("\r", '', trim($text));
        $text = preg_replace("#^\n[ \t]+[*]?#i", "\n", trim($text));
        $text = preg_replace("#[\t ]+#i", ' ', trim($text));
        $text = str_replace("\"", "\\\"", $text);

        // let's clean the doc block
        // $text = str_replace('/**', '', $text);
        // $text = str_replace('*/', '', $text);
        // $text = preg_replace('/^\s*\* ?/m', '', $text);

        $parts = explode("\n", $text, 2);
        $parts[0] = trim($parts[0]);
        $parts[1] = isset($parts[1]) ? trim($parts[1]) : '';

        if (('' === $parts[0]) && isset($var[2])) {
            $parts[0] = trim($var[2]);
        }

        if ('' !== $parts[1]) {
            $parts[1] = preg_replace("#\s*\n\s*\n\s*#", '@@KEEPPARAGRAPHS@@', $parts[1]);
            $parts[1] = preg_replace("#\s*\n\s*#", ' ', $parts[1]);
            $parts[1] = str_replace('@@KEEPPARAGRAPHS@@', "\n", $parts[1]);

            return array('title' => $parts[0], 'description' => $parts[1]);
        } elseif ('' !== $parts[0]) {
            return array('title' => null, 'description' => $parts[0]);
        } else {
            return array('title' => null, 'description' => null);
        }

    }

    protected function getType(Reflector $element)
    {
        $type = ($element instanceof ReflectionMethod)
            ? $this->getReturnAnnotation($element)
            : $this->getVarAnnotation($element);

        $result = array('type' => $type, 'original_type' => $type, 'is_array' => false);

        if (null === $type) {
            return $result;
        }

        // Checks if the property has @var array array<type> or type[] annotation
        if ('array' === $type) {
            // unclassified arrays are interpreted as being untyped
            $result['type'] = null;
            $result['is_array'] = true;
        } elseif ($type && false !== $pos = strpos($type, '<')) {
            $result['type'] = substr($type, $pos + 1, -1);
            $result['is_array'] = true;
        } elseif ('[]' === substr($type, -2)) {
            $result['type'] = substr($type, 0, -2);
            $result['is_array'] = true;
        }

        return $result;
    }

    /**
     * Extracts the type of a property using the @var annotation
     *
     * @param  ReflectionProperty $property The property.
     * @return array The property's type.
     */
    protected function getVarAnnotation(ReflectionProperty $property)
    {
        $result = array();
        // Adapted from Doctrine\Common\Annotations\DocParser::collectAnnotationMetadata()

        // checks if the property has @var annotation
        if ((false !== $propertyComment = $property->getDocComment())
            && false !== strpos($propertyComment, '@var')
            && preg_match('/@var\s+([^\s]+)((?:[ ]+)[^\r\n]*)?/', $propertyComment, $matches)) {   // TODO Fix regex (line end)
            // literal type declaration
            $value = $matches[1];

            return $value;
        }

        return null;
    }

    protected function getReturnAnnotation(ReflectionMethod $method)
    {
        $result = array();
        // Adapted from Doctrine\Common\Annotations\DocParser::collectAnnotationMetadata()

        // checks if the property has @return annotation
        if ((false !== $methodComment = $method->getDocComment())
            && false !== strpos($methodComment, '@return')
            && preg_match('/@return\s+([^\s]+)((?:[ ]+)[^\r\n]*)?/', $methodComment, $matches)) {   // TODO Fix regex (line end)
            // literal type declaration
            $value = $matches[1];

            return $value;
        }

        return null;
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use ML\HydraBundle\Serializer\Serializer;
use ML\HydraBundle\JsonLdResponse;

/**
 * HydraController is a implementation of a Controller for Hydra
 *
 * It provides methods to common features needed in Hydra controllers.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class HydraController extends Controller
{
    /**
     * The JSON-LD format
     */
    const FORMAT = 'jsonld';

    /**
     * Shortcut to serialize an entity into JSON-LD
     *
     * This method uses the Hydra Serializer service and therefore requires
     * that the HydraBundle is registered.
     *
     * @param object $entity The entity to serialize
     *
     * @return string The entity serialized in JSON-LD
     *
     * @throws \LogicException If HydraBundle is not available
     */
    public function serialize($entity)
    {
        if (!$this->container->has('hydra.serializer')) {
            throw new \LogicException('The HydraBundle is not registered in your application.');
        }

        return $this->container->get('hydra.serializer')->serialize($entity, self::FORMAT);
    }

    /**
     * Shortcut to deserialize JSON-LD data to an entity
     *
     * This method uses the Hydra Serializer service and therefore requires
     * that the HydraBundle is registered.
     *
     * @param string        $data   The data to deserialize
     * @param string|object $entity The class or an instance thereof the data
     *                              should be deserialized to.
     *
     * @return object The deserialized entity
     *
     * @throws \LogicException If HydraBundle is not available
     */
    public function deserialize($data, $entity)
    {
        if (!$this->container->has('hydra.serializer')) {
            throw new \LogicException('The HydraBundle is not registered in your application.');
        }

        $serializer = $this->container->get('hydra.serializer');

        if (is_object($entity)) {
            return $serializer->deserializeIntoEntity($data, $entity);
        }

        return $serializer->deserialize($data, $entity, self::FORMAT);
    }

    /**
     * Shortcut to validate an entity
     *
     * This method uses the Validator service. It throws an Exception with
     * a status code of "400 Bad Request" in case of validation errors.
     *
     * @param object $entity The entity to validate.
     *
     * @return false|Error If false is returned, no validation errors have
     *                     been found, otherwise an Error response is
     *                     returned.
     *
     * @throws \LogicException If the validator service is not available
     */
    public function validate($entity)
    {
        if (!$this->container->has('validator')) {
            throw new \LogicException('The validator service is not available.');
        }

        $errors = $this->container->get('validator')->validate($entity);

        if (count($errors) === 0) {
            return false;
        }

        // TODO Use Hydra Error instead
        return new JsonLdResponse('{ "error": "Validation error" }', 400);
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\Mapping;

use Symfony\Component\PropertyAccess\StringUtil;

/**
 * A Hydra Property definition
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class PropertyDefinition
{
    /**
     * Constant used to specify that the getter/setter is a property
     */
    const GETTER_SETTER_PROPERTY = 1;

    /**
     * Constant used to specify that the getter/setter is a method
     */
    const GETTER_SETTER_METHOD = 2;

    /**
     * @var string The fully-qualified name of the class the property belongs to
     */
    private $class;

    /**
     * @var string The name of the property
     */
    private $name;

    /**
     * @var string The name that should be used when serializing this property
     */
    private $exposeAs;

    /**
     * @var string The IRI used to identify this property
     */
    private $iri;

    /**
     * @var string The title of the description of this property
     */
    private $title;

    /**
     * @var string A description of this property
     */
    private $description;

    /**
     * @var string The value type of this property (either a primitive type
     *             or a fully-qualified class name)
     */
    private $type;

    /**
     * @var boolean Is this a required property?
     */
    private $required;

    /**
     * @var boolean Is this a read-only property?
     */
    private $readOnly;

    /**
     * @var boolean Is this a write-only property?
     */
    private $writeOnly;

    /**
     * @var string The route to create IRIs identifying instances of this property
     */
    private $route;

    /**
     * @var array Mapping of route variables to members and methods (property paths)
     */
    private $routeVariableMappings;

    /**
     * @var array The operations known to be supported by instances of this property
     */
    private $operations = array();

    /**
     * @var string The name of the getter to get a value of this property from an entity
     */
    private $getter;

    /**
     * Type of the getter
     *
     * One of {@link PropertyDefinition::GETTER_SETTER_PROPERTY} or
     * {@link PropertyDefinition::GETTER_SETTER_METHOD}.
     *
     * @var integer
     */
    private $getterType;

    /**
     * @var string The name of the setter to set a value of this property on an entity
     */
    private $setter;

    /**
     * Type of the setter
     *
     * One of {@link PropertyDefinition::GETTER_SETTER_PROPERTY} or
     * {@link PropertyDefinition::GETTER_SETTER_METHOD}.
     *
     */
    private $setterType;

    /**
     * @var string The name of the adder/remove to add or remove entries from an array value
     */
    private $adderRemover;

    /**
     * Constructor
     *
     * @param string $class The fully-qualified name of the class the
     *                      property belongs to.
     * @param string $name  The name of the property being documented.
     */
    public function __construct($class, $name)
    {
        $this->class = $class;
        $this->name = $name;

        $this->findGetter();
        $this->findSetter();
        $this->findAdderAndRemover();

        $this->readOnly = false;
        $this->writeOnly = false;

        if ((null === $this->getter) && ((null !== $this->setter) || (null !== $this->adderRemover))) {
            $this->writeOnly = true;
        } elseif ((null !== $this->getter) && (null === $this->setter) && (null === $this->adderRemover)) {
            $this->readOnly = true;
        }
    }

    /**
     * Gets the fully-qualified name of the class this property belongs to
     *
     * @return string The fully-qualified name of the class this property belongs to.
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * Gets the name of this property
     *
     * @return string The the name of this property.
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Sets the name to be used when serializing this property
     *
     * @return string The name to be used when serializing this property.
     *
     * @return PropertyDefinition $this
     */
    public function setExposeAs($exposeAs)
    {
        $this->exposeAs = $exposeAs;

        return $this;
    }

    /**
     * Gets the name to be used when serializing this property
     *
     * @return string The name to be used when serializing this property.
     */
    public function getExposeAs()
    {
        return $this->exposeAs;
    }

    /**
     * Sets the IRI identifying this property
     *
     * @return string The IRI identifying this property.
     *
     * @return PropertyDefinition $this
     */
    public function setIri($iri)
    {
        $this->iri = $iri;

        return $this;
    }

    /**
     * Gets the IRI identifying this property
     *
     * @return string The IRI identifying this property.
     */
    public function getIri()
    {
        return $this->iri;
    }

    /**
     * Does this property definition represent a reference to an external
     * definition?
     *
     * An external definition can be used but not modified or further
     * annotated as it is not under the control of this system.
     *
     * @return boolean True if this property definition represents an
     *                 external reference, false otherwise.
     */
    public function isExternalReference()
    {
        return strpos($this->iri, ':') !== false;
    }

    /**
     * Sets the title of the description of this property
     *
     * @param string $title The title of the description of this property.
     *
     * @return PropertyMetadata $this
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Gets the title of the description of this property
     *
     * @return string The title of the description of this property.
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Sets the description of this property
     *
     * @param string $description The description of this property.
     *
     * @return PropertyMetadata $this
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Gets the description of this property
     *
     * @return string The description of this property.
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Sets the type of this property
     *
     * This is either a fully-qualified class name, a primitive type, or
     * an absolute IRI. Absolute IRIs can be detected by looking for a colon.
     *
     * @param string $type The type of this property.
     *
     * @return PropertyMetadata $this
     */
    public function setType($type)
    {
        $this->type = $type;

        return $this;
    }

    /**
     * Gets the type of this property
     *
     * This is either a fully-qualified class name, a primitive type, or
     * an absolute IRI. Absolute IRIs can be detected by looking for a colon.
     *
     * @return string The type of this property.
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Is the property required?
     *
     * @param boolean $required Is this a required property
     *
     * @return PropertyMetadata $this
     */
    public function setRequired($required)
    {
        $this->required = $required;

        return $this;
    }

    /**
     * Is the property required?
     *
     * @return boolean True if the property is required; otherwise false.
     */
    public function getRequired()
    {
        return $this->required;
    }

    /**
     * Is the property read-only?
     *
     * @param boolean $readOnly Is this a read-only property
     *
     * @return PropertyMetadata $this
     */
    public function setReadOnly($readOnly)
    {
        $this->readOnly = (boolean)$readOnly;

        return $this;
    }

    /**
     * Is the property read-only?
     *
     * @return boolean True if the property is read-only; otherwise false.
     */
    public function isReadOnly()
    {
        return $this->readOnly;
    }

    /**
     * Is the property write-only?
     *
     * @param boolean $writeOnly Is this a write-only property
     *
     * @return PropertyMetadata $this
     */
    public function setWriteOnly($writeOnly)
    {
        $this->writeOnly = (boolean)$writeOnly;

        return $this;
    }

    /**
     * Is the property write-only?
     *
     * @return boolean True if the property is write-only; otherwise false.
     */
    public function isWriteOnly()
    {
        return $this->writeOnly;
    }

    /**
     * Sets the route to create IRIs identifying instances of this property
     *
     * @param string $route The route to create IRIs identifying instances
     *                      of this property.
     *
     * @return PropertyMetadata $this
     */
    public function setRoute($route)
    {
        $this->route = $route;

        return $this;
    }

    /**
     * Gets the route to create IRIs identifying instances of this property
     *
     * @return string The route to create IRIs identifying instances of this
     *                property.
     */
    public function getRoute()
    {
        return $this->route;
    }

    /**
     * Sets the operations known to be supported by instances of this property
     *
     * @param array $operations The operations known to be supported by
     *                          instances of this property.
     *
     * @return PropertyMetadata $this
     */
    public function setOperations(array $operations)
    {
        $this->operations = $operations;

        return $this;
    }

    /**
     * Adds an operation known to be supported by instances of this property
     *
     * @param array $operations The operation known to be supported by
     *                          instances of this property.
     *
     * @return PropertyMetadata $this
     */
    public function addOperation(OperationDefinition $operation)
    {
        if (false === $this->supportsOperation($operation->getName())) {
            $this->operations[] = $operation;
        }

        return $this;
    }

    /**
     * Gets the operations known to be supported by instances of this property
     *
     * @return array The operations known to be supported by instances of
     *               this property.
     */
    public function getOperations()
    {
        return $this->operations;
    }

    /**
     * Checks whether a specific operation is known to be supported
     *
     * @param string $operationName The name of the operation.
     *
     * @return boolean True if the operation is supported, false otherwise.
     */
    public function supportsOperation($operationName)
    {
        foreach ($this->operations as $operation) {
            if ($operation->getName() === $operationName) {
                return true;
            }
        }

        return false;
    }

    /**
     * Has a setter be found for this property
     *
     * @return boolean True if a setter has been found, false otherwise.
     */
    public function hasSetter()
    {
        return (null !== $this->setter);
    }

    /**
     * Has a getter be found for this property
     *
     * @return boolean True if a getter has been found, false otherwise.
     */
    public function hasGetter()
    {
        return (null !== $this->setter);
    }

    /**
     * Sets this property on the given entity to the specified value
     *
     * @param object $entity The entity.
     * @param mixed $value   The value.
     *
     * @throws \Exception If no setter has been found or the entity is of
     *                    the wrong type.
     */
    public function setValue($entity, $value)
    {
        if (false === ($entity instanceof $this->class)) {
            // FIXXME Improve this message
            throw new \Exception(
                "Can't set the entity's {$this->name} property as the entity is not an instance of {$this->class}."
            );
        }

        if (!is_array($value) && !($value instanceof \Traversable)) {
            if ((null !== $this->adderRemover) && (null !== $this->getter)) {
                // Use iterator_to_array() instead of clone in order to prevent side effects
                // see https://github.com/symfony/symfony/issues/4670
                $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
                $itemToRemove = array();
                $previousValue = $this->getValue($entity);

                if (is_array($previousValue) || $previousValue instanceof \Traversable) {
                    foreach ($previousValue as $previousItem) {
                        foreach ($value as $key => $item) {
                            if ($item === $previousItem) {
                                // Item found, don't add
                                unset($itemsToAdd[$key]);

                                // Next $previousItem
                                continue 2;
                            }
                        }

                        // Item not found, add to remove list
                        $itemToRemove[] = $previousItem;
                    }
                }

                foreach ($itemToRemove as $item) {
                    call_user_func(array($entity, 'remove' . $this->adderRemover), $item);
                }

                foreach ($itemsToAdd as $item) {
                    call_user_func(array($entity, 'add' . $this->adderRemover), $item);
                }

                return;
            }
        }

        if (null === $this->setter) {
            // FIXXME Improve this message
            throw new \Exception(
                "Can't set the entity's {$this->name} property as no setter has been found."
            );
        }

        if (self::GETTER_SETTER_METHOD === $this->setterType) {
            return $entity->{$this->setter}($value);
        } else {
            return $entity->{$this->setter} = $value;
        }
    }

    /**
     * Gets this property's value of the given entity
     *
     * @param object $entity The entity.
     *
     * @return mixed The property's value.
     */
    public function getValue($entity)
    {
        if (null === $this->getter) {
            // FIXXME Improve this message
            throw new \Exception(
                "Can't get the entity's {$this->name} property as no getter has been found."
            );
        } elseif (false === ($entity instanceof $this->class)) {
            // FIXXME Improve this message
            throw new \Exception(
                "Can't get the entity's {$this->name} property as the entity is not an instance of {$this->class}."
            );
        }

        if (self::GETTER_SETTER_METHOD === $this->getterType) {
            return $entity->{$this->getter}();
        } else {
            return $entity->{$this->getter};
        }
    }

    /**
     * Try to find the getter associated to this property
     *
     * Simplified version of {@link Symfony\Component\PropertyAccess\PropertyAccessor}.
     */
    private function findGetter()
    {
        $reflClass = new \ReflectionClass($this->class);
        $camelProp = $this->camelize($this->name);

        // Try to find a getter
        $getter = 'get'.$camelProp;
        $isser = 'is'.$camelProp;
        $hasser = 'has'.$camelProp;
        $classHasProperty = $reflClass->hasProperty($this->name);

        if ($reflClass->hasMethod($this->name) && $reflClass->getMethod($this->name)->isPublic()) {
            $this->getter = $this->name;
            $this->getterType = self::GETTER_SETTER_METHOD;
        } elseif ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
            $this->getter = $getter;
            $this->getterType = self::GETTER_SETTER_METHOD;
        } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
            $this->getter = $isser;
            $this->getterType = self::GETTER_SETTER_METHOD;
        } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
            $this->getter = $hasser;
            $this->getterType = self::GETTER_SETTER_METHOD;
        } elseif (($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) ||
                  ($classHasProperty && $reflClass->getProperty($this->name)->isPublic())) {
            $this->getter = $this->name;
            $this->getterType = self::GETTER_SETTER_PROPERTY;
        // } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
        //     // we call the getter and hope the __call do the job
        //     $result[self::VALUE] = $object->$getter();
        }
    }

    /**
     * Camelizes a given string
     *
     * Turns something like property_name into propertyName.
     *
     * Copied from {@link Symfony\Component\PropertyAccess\PropertyAccessor::camelize()}.
     *
     * @param string $string Some string.
     *
     * @return string The camelized version of the string.
     */
    private function camelize($string)
    {
        return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string);
    }

    /**
     * Try to find the setter associated to this property
     *
     * Simplified version of {@link Symfony\Component\PropertyAccess\PropertyAccessor}.
     */
    private function findSetter()
    {
        $reflClass = new \ReflectionClass($this->class);

        $setter = 'set' . $this->camelize($this->name);
        $classHasProperty = $reflClass->hasProperty($this->name);

        if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) {
            $this->setter = $setter;
            $this->setterType = self::GETTER_SETTER_METHOD;
        } elseif ((0 === strpos($this->name, 'set')) && $reflClass->hasMethod($this->name) && $reflClass->getMethod($this->name)->isPublic()) {
            $this->setter = $this->name;
            $this->setterType = self::GETTER_SETTER_METHOD;
        } elseif (($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) ||
                  ($classHasProperty && $reflClass->getProperty($this->name)->isPublic())) {
            $this->setter = $this->name;
            $this->setterType = self::GETTER_SETTER_PROPERTY;
        // } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
        //     // we call the getter and hope the __call do the job
        //     $object->$setter($value);
        }
    }

    /**
     * Searches add and remove methods
     *
     * Simplified version of {@link Symfony\Component\PropertyAccess\PropertyAccessor}.
     */
    private function findAdderAndRemover()
    {
        $reflClass = new \ReflectionClass($this->class);
        $singulars = (array) StringUtil::singularify($this->camelize($this->name));

        foreach ($singulars as $singular) {
            $addMethod = 'add'.$singular;
            $removeMethod = 'remove'.$singular;

            $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
            $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);

            if ($addMethodFound && $removeMethodFound) {
                $this->adderRemover = $singular;

                return;
            }
        }
    }

    /**
     * Returns whether a method is public and has a specific number of required parameters.
     *
     * Copied from {@link Symfony\Component\PropertyAccess\PropertyAccessor}.
     *
     * @param  \ReflectionClass $class      The class of the method
     * @param  string           $methodName The method name
     * @param  integer          $parameters The number of parameters
     *
     * @return Boolean Whether the method is public and has $parameters
     *                                      required parameters
     */
    private function isAccessible(\ReflectionClass $class, $methodName, $parameters)
    {
        if ($class->hasMethod($methodName)) {
            $method = $class->getMethod($methodName);

            if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
                return true;
            }
        }

        return false;
    }
}


<?php

/*
 * (c) Markus Lanthaler <mail@markus-lanthaler.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ML\HydraBundle\Mapping;

use ML\HydraBundle\Mapping\NamingStrategy;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Util\ClassUtils;

/**
 * ClassMetadataFactory
 *
 * The ClassMetadataFactory is used to create ClassMetadata objects that
 * contain all the metadata mapping information of a class which describes
 * how a class is mapped to Hydra.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class ClassMetadataFactory
{
    /**
     * @var \Doctrine\Common\Persistence\Mapping\Driver\MappingDriver
     */
    private $driver;

    /**
     * @var \Doctrine\Common\Cache\Cache
     */
    private $cacheDriver;

    /**
     * @var string Salt used for caching.
     */
    protected $cacheSalt = "\$HYDRA-CLASSMETADATA";

    /**
     * @var NamingStrategy
     */
    private $namingStrategy;

    /**
     * @var array
     */
    private $loadedMetadata = array();

    /**
     * Constructor
     *
     * object $driver      The mapping driver.
     * Cache  $cacheDriver Cache driver used by the factory to cache
     *                     ClassMetadata instances.
     */
    public function __construct($driver, Cache $cacheDriver, NamingStrategy $namingStrategy)
    {
        $this->driver = $driver;
        $this->cacheDriver = $cacheDriver;
        $this->namingStrategy = $namingStrategy;
    }

    /**
     * Return the mapping driver.
     *
     * @return object The mapping driver.
     */
    protected function getDriver()
    {
        return $this->driver;
    }

    /**
     * Sets the cache driver used by the factory to cache ClassMetadata
     * instances.
     *
     * @param Doctrine\Common\Cache\Cache $cacheDriver
     */
    public function setCacheDriver(Cache $cacheDriver = null)
    {
        $this->cacheDriver = $cacheDriver;
    }

    /**
     * Gets the cache driver used by the factory to cache ClassMetadata instances.
     *
     * @return Doctrine\Common\Cache\Cache
     */
    public function getCacheDriver()
    {
        return $this->cacheDriver;
    }

    /**
     * Forces the factory to load the metadata of all classes known to the underlying
     * mapping driver.
     *
     * @return array The ClassMetadata instances of all mapped classes.
     */
    public function getAllMetadata()
    {
        // FIXXME Should this be implemented here or in the driver (chain)?
        $metadata = array();
        foreach ($this->driver->getAllClassNames() as $className) {
            $metadata[] = $this->getMetadataFor($className);
        }

        $this->validate($metadata);

        return $metadata;
    }

    /**
     * Gets the class metadata for the specified class
     *
     * @param string $className The name of the class.
     * @return ClassMetadata The metadata.
     */
    public function getMetadataFor($className)
    {
        if (isset($this->loadedMetadata[$className])) {
            return $this->loadedMetadata[$className];
        }

        $realClassName = ClassUtils::getRealClass($className);

        if (isset($this->loadedMetadata[$realClassName])) {
            // We do not have the alias name in the map, include it
            $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];

            return $this->loadedMetadata[$realClassName];
        }

        if ($this->cacheDriver) {
            if (($cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt)) !== false) {
                $this->loadedMetadata[$realClassName] = $cached;
            } else {
                $this->cacheDriver->save(
                    $realClassName . $this->cacheSalt, $this->loadMetadata($realClassName), null
                );
            }
        } else {
            $this->loadMetadata($realClassName);
        }

        if ($className != $realClassName) {
            // We do not have the alias name in the map, include it
            $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
        }

        return $this->loadedMetadata[$className];
    }

    /**
     * Loads the metadata of the specified class
     *
     * @param string $nameName The name of the class for which the metadata
     *                         should be loaded.
     *
     * @return ClassMetadata
     */
    protected function loadMetadata($className)
    {
        if (false === isset($this->loadedMetadata[$className])) {
            if (null === ($class = $this->driver->loadMetadataForClass($className))) {
                // FIXXME Improve this
                throw new \Exception("Can't load metadata for $className");
            }

            $this->completeMetadata($class);
            $this->loadedMetadata[$className] = $class;
        }

        return $this->loadedMetadata[$className];
    }

    /**
     * Completes the metadata by setting missing fields that can be inferred
     * by other fields
     *
     * Furthermore, the type of properties which is returned as string by
     * the metadata driver is replaced with a ClassMetadata instance.
     *
     * @param ClassMetadata $class The metadata to complete
     */
    protected function completeMetadata(ClassMetadata $class)
    {
        $className = $class->getName();

        if (null === $class->getIri()) {
            $class->setIri($this->namingStrategy->classIriFragment($className));
        }

        if (null === $class->getExposeAs()) {
            $class->setExposeAs($this->namingStrategy->classShortName($className));
        }

        // If no title has been set for this class, use it's short name
        if (null === $class->getTitle()) {
            $class->setTitle($this->namingStrategy->classShortName($className));
        }

        foreach ($class->getProperties() as $property) {
            $propertyName = $property->getName();

            if (null === $property->getIri()) {
                $property->setIri(
                    $this->namingStrategy->propertyIriFragment($className, $propertyName)
                );
            }

            if (null === $property->getExposeAs()) {
                $property->setExposeAs(
                    $this->namingStrategy->propertyShortName($className, $propertyName)
                );
            }

            // If no title has been set for this property, use it's short name
            if (null === $property->getTitle()) {
                $property->setTitle(
                    $this->namingStrategy->propertyShortName($className, $propertyName)
                );
            }
        }
    }

    /**
     * Completes the metadata of operations by replacing class references
     *
     * @param  [type] $operations   [description]
     *
     * @return [type] [description]
     */
    private function completeOperationsMetadata($operations)
    {
        // FIXXME Implement this
    }

    /**
     * Validates whether the metadata is consistent and complete
     *
     * @param  array<ClassMetadata> $metadata The metadata to validate
     *
     * @return boolean True if the metadata is valid, false otherwise
     */
    private function validate($metadata)
    {
        // FIXXME Implement this

        // Check operations expects/returns, properties' type

        // Verify that route's return type corresponds with the property's type

        // Verify that all operations have the same IRI template

        // Look for collisions: IRI, exposeAs
    }
}