<?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\Serializer;

use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use ML\JsonLD\JsonLD;
use Doctrine\Common\Util\ClassUtils;
use ML\HydraBundle\DocumentationGenerator;
use ML\HydraBundle\HydraApi;
use ML\HydraBundle\Mapping\OperationDefinition;

/**
 * JSON-LD serializer
 *
 * Serializes annotated objects to a JSON-LD representation.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class Serializer implements SerializerInterface
{
    /**
     * @var HydraApi The Hydra documentation.
     */
    private $hydraApi;

    /**
     * @var RouterInterface The router.
     */
    private $router;

    /**
     * Constructor
     *
     * @param HydraApi        $hydraApi The Hydra documentation.
     * @param RouterInterface $router   The router.
     */
    public function __construct(HydraApi $hydraApi, RouterInterface $router)
    {
        $this->hydraApi = $hydraApi;
        $this->router = $router;
    }

    /**
     * Serializes data in the appropriate format
     *
     * @param mixed  $data    any data
     * @param string $format  format name
     * @param array  $context options normalizers/encoders have access to
     *
     * @return string
     */
    public function serialize($data, $format, array $context = array())
    {
        if ('jsonld' !== $format) {
            throw new UnexpectedValueException('Serialization for the format ' . $format . ' is not supported');
        }

        if (false === is_object($data)) {
            throw new \Exception('Only objects can be serialized');
        }

        return JsonLD::toString($this->doSerialize($data, true), true);
    }

    /**
     * Serializes data
     *
     * @param mixed    $data    The data to serialize.
     * @param boolealn $include Include (embed) the complete data instead of
     *                          of just adding a reference?
     *
     * @return mixed The serialized data.
     */
    private function doSerialize($data, $include = false)
    {
        // TODO Handle cycles and allow subtrees to be embedded instead of just being referenced
        $metadata = $this->hydraApi->getMetadataFor(get_class($data));

        if (null === $metadata) {
            // TODO Improve this error message
            throw new \Exception(sprintf('"%s" cannot be serialized as it is not documented.', get_class($data)));
        }

        $result = array();
        if ($include) {
            $result['@context'] = $this->router->generate('hydra_context', array('type' => $metadata->getExposeAs()));
        }

        if (null !== ($route = $metadata->getRoute())) {
            if (null !== ($url = $this->generateUrl($route, $data))) {
                $result['@id'] = $url;
            }
        }

        if ($include) {
            $result['@type'] = $metadata->getExposeAs();
        } else {
            $result['@type'] = ($metadata->isExternalReference())
                ? $metadata->getIri()
                : 'vocab:' . $metadata->getIri();
        }

        if (false === $include) {
            return $result;
        }

        foreach ($metadata->getProperties() as $property) {
            if ($property->isWriteOnly()) {
                continue;
            }

            $value = $property->getValue($data);

            if (null !== ($route = $property->getRoute())) {
                if (false === $value) {
                    continue;
                }

                $url = $this->generateUrl($route, $data, $value);

                if (null !== $url) {
                    $result[$property->getExposeAs()] = $url;
                }

                continue;
            }

            // TODO Recurse
            if (is_object($value) && $this->hydraApi->hasNormalizer(get_class($value))) {
                $normalizer = $this->hydraApi->getNormalizer(get_class($value));
                $result[$property->getExposeAs()] = $normalizer->normalize($value);
            } elseif (is_array($value) || ($value instanceof \ArrayAccess) || ($value instanceof \Travesable)) {
                $result[$property->getExposeAs()] = array();
                foreach ($value as $val) {
                    $result[$property->getExposeAs()][] = $this->doSerialize($val);
                }
            } else {
                $result[$property->getExposeAs()] = $value;
            }
        }

        return $result;
    }

    /**
     * Generate a URL for the specified operation using the passed entity/data
     *
     * @param OperationDefinition $operation    The operation specifying the route
     * @param mixed               $entity       The entity to be used to fill URL variables
     * @param mixed               $value        A value to be used to fill URL variables
     *
     * @return string|null The URL or null if the URL shouldn't be exposed.
     */
    private function generateUrl(OperationDefinition $operation, $entity, $value = null)
    {
        $route = $operation->getRoute();
        $routeVariables = $route->compile()->getVariables();
        $variableValues = $route->getDefaults();
        unset($variableValues['_controller']);

        // TODO: Allow custom route variable mappings
        if (is_array($value)) {
            $variableValues += $value;  // FIXXME: Check if this is really what we want in all cases
        } elseif (1 === count($routeVariables)) {
            if (is_scalar($value)) {
                $variableValues[reset($routeVariables)] = $value;
            } elseif (is_object($value) && is_callable(array($value, 'getId'))) {
                // TODO Make the is_callable check more robust
                $variableValues[reset($routeVariables)] = $value->getId();
            } elseif (is_object($entity) && is_callable(array($entity, 'getId'))) {
                // TODO Check if this is want in all cases
                $variableValues[reset($routeVariables)] = $entity->getId();
            } elseif (null === $value) {
                return null;
            }
        } else {
            $accessor = PropertyAccess::createPropertyAccessor();

            foreach ($routeVariables as $variable) {
                try {
                    $variableValues[$variable] = $accessor->getValue($value, $variable);
                } catch (\Exception $e) {
                    // do nothing, no such property exists
                }
            }

        }

        return $this->router->generate($operation->getName(), $variableValues);
    }

    /**
     * Deserializes data into the given type.
     *
     * @param mixed  $data
     * @param string $type
     * @param string $format
     * @param array  $context
     *
     * @return object
     */
    public function deserialize($data, $type, $format, array $context = array())
    {
        if ('jsonld' !== $format) {
            throw new UnexpectedValueException('Deserialization for the format ' . $format . ' is not supported');
        }

        $reflectionClass = new \ReflectionClass($type);

        if (null !== ($constructor = $reflectionClass->getConstructor())) {
            if (0 !== $constructor->getNumberOfRequiredParameters()) {
                throw new RuntimeException(
                    'Cannot create an instance of '. $type .
                    ' from serialized data because its constructor has required parameters.'
                );
            }
        }

        return $this->doDeserialize($data, new $type);
    }

    /**
     * Deserializes data into an existing entity
     *
     * @param mixed  $data   The data to deserialize.
     * @param object $entity The entity into which the data should be
     *                       deserialized.
     *
     * @return object The entity.
     */
    public function deserializeIntoEntity($data, $entity)
    {
        return $this->doDeserialize($data, $entity);
    }

    /**
     * Deserializes JSON-LD data
     *
     * @param mixed  $data   The data to deserialize.
     * @param object $entity The entity into which the data should be
     *                       deserialized.
     *
     * @return object The entity.
     */
    private function doDeserialize($data, $entity)
    {
        $metadata = $this->hydraApi->getMetadataFor(get_class($entity));

        if (null === $metadata) {
            // TODO Improve this error message
            throw new \Exception(sprintf('"%s" cannot be serialized as it is not documented.', get_class($data)));
        }

        $vocabPrefix = $this->router->generate('hydra_vocab', array(), true) . '#';
        $typeIri = ($metadata->isExternalReference())
            ? $metadata->getIri()
            : $vocabPrefix . $metadata->getIri();

        $graph = JsonLD::getDocument($data)->getGraph();
        $node = $graph->getNodesByType($typeIri);

        if (1 !== count($node)) {
            throw new RuntimeException(
                'The passed data contains '. count($node) . ' nodes of the type ' .
                $typeIri . '; expected 1.'
            );
        }

        $node = reset($node);

        foreach ($metadata->getProperties() as $property) {
            if ($property->isReadOnly()) {
                continue;
            }

            // TODO Parse route!
            if (null !== ($route = $property->getRoute())) {
                continue;   // FIXXE Handle properties whose value are URLs
            }

            // TODO Recurse!?
            $propertyIri = ($property->isExternalReference())
                ? $property->getIri()
                : $vocabPrefix . $property->getIri();

            $value = $node->getProperty($propertyIri);
            if ($value instanceof \ML\JsonLD\Value) {
                $value = $value->getValue();
            }

            if (!is_null($value) && $this->hydraApi->hasNormalizer($property->getType())) {
                $normalizer = $this->hydraApi->getNormalizer($property->getType());
                $value = $normalizer->denormalize($value, $property->getType());
            }

            $property->setValue($entity, $value);  // TODO Fix IRI construction

            // if (is_array($value) || ($value instanceof \ArrayAccess) || ($value instanceof \Travesable)) {
            //     $result[$property] = array();
            //     foreach ($value as $val) {
            //         $result[$property][] = $this->doSerialize($val);
            //     }
            // } else {
            //     $result[$property] = $value;
            // }
        }

        return $entity;
    }
}


<?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\Routing\Route as SfRoute;
use Symfony\Component\Routing\RouterInterface;

/**
 * A Hydra Class definition
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class ClassMetadata
{
    /**
     * @var string The fully-qualified name of this class
     */
    private $name;

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

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

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

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

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

    /**
     * @var array The properties known to be supported by instances of this class
     */
    private $properties;

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

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

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

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

        return $this;
    }

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

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

        return $this;
    }

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

    /**
     * Does this class 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 class definition represents an external
     *                 reference, false otherwise.
     */
    public function isExternalReference()
    {
        return strpos($this->iri, ':') !== false;
    }

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

        return $this;
    }

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

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

        return $this;
    }

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

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

        return $this;
    }

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

    /**
     * Sets the properties known to be supported by instances of this class
     *
     * @param array $properties The properties known to be supported by
     *                          instances of this class.
     *
     * @return ClassMetadata $this
     */
    public function setProperties($properties)
    {
        $this->properties = $properties;

        return $this;
    }

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

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

        return $this;
    }

    /**
     * Adds an operation known to be supported by instances of this class
     *
     * @param array $operations The operation known to be supported by
     *                          instances of this class.
     *
     * @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 class
     *
     * @return array The operations known to be supported by instances of
     *               this class.
     */
    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;
    }

    /**
     * Sets the specified property to the specified value on the given entity
     *
     * @param object $entity
     * @param string $property
     * @param mixed $value
     */
    public function setPropertyValue($entity, $property, $value)
    {
        $this->reflFields[$property]->setValue($entity, $value);
    }

    /**
     * Gets the specified property's value of the given entity
     *
     * @param object $entity
     * @param string $property
     */
    public function getPropertyValue($entity, $property)
    {
        return $this->reflFields[$property]->getValue($entity);
    }

    /**
     * Determines which fields get serialized.
     *
     * It is only serialized what is necessary for best unserialization performance.
     * That means any metadata properties that are not set or empty or simply have
     * their default value are NOT serialized.
     *
     * Parts that are also NOT serialized because they can not be properly unserialized:
     *      - reflClass (ReflectionClass)
     *      - reflFields (ReflectionProperty array)
     *
     * @return array The names of all the fields that should be serialized.
     */
    public function __sleep()
    {
        // This metadata is always serialized/cached.
        $serialized = array(
            'associationMappings',
            'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
            'fieldMappings',
            'fieldNames',
            'identifier',
            'isIdentifierComposite', // TODO: REMOVE
            'name',
            'namespace', // TODO: REMOVE
            'table',
            'rootEntityName',
            'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
        );

        // The rest of the metadata is only serialized if necessary.
        if ($this->changeTrackingPolicy != self::CHANGETRACKING_DEFERRED_IMPLICIT) {
            $serialized[] = 'changeTrackingPolicy';
        }

        if ($this->customRepositoryClassName) {
            $serialized[] = 'customRepositoryClassName';
        }

        if ($this->inheritanceType != self::INHERITANCE_TYPE_NONE) {
            $serialized[] = 'inheritanceType';
            $serialized[] = 'discriminatorColumn';
            $serialized[] = 'discriminatorValue';
            $serialized[] = 'discriminatorMap';
            $serialized[] = 'parentClasses';
            $serialized[] = 'subClasses';
        }

        if ($this->generatorType != self::GENERATOR_TYPE_NONE) {
            $serialized[] = 'generatorType';
            if ($this->generatorType == self::GENERATOR_TYPE_SEQUENCE) {
                $serialized[] = 'sequenceGeneratorDefinition';
            }
        }

        if ($this->isMappedSuperclass) {
            $serialized[] = 'isMappedSuperclass';
        }

        if ($this->containsForeignIdentifier) {
            $serialized[] = 'containsForeignIdentifier';
        }

        if ($this->isVersioned) {
            $serialized[] = 'isVersioned';
            $serialized[] = 'versionField';
        }

        if ($this->lifecycleCallbacks) {
            $serialized[] = 'lifecycleCallbacks';
        }

        if ($this->namedQueries) {
            $serialized[] = 'namedQueries';
        }

        if ($this->namedNativeQueries) {
            $serialized[] = 'namedNativeQueries';
        }

        if ($this->sqlResultSetMappings) {
            $serialized[] = 'sqlResultSetMappings';
        }

        if ($this->isReadOnly) {
            $serialized[] = 'isReadOnly';
        }

        if ($this->customGeneratorDefinition) {
            $serialized[] = "customGeneratorDefinition";
        }

        return $serialized;
    }
}


<?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;

use Symfony\Component\HttpFoundation\Response;

/**
 * JsonLdResponse represents an HTTP response in JSON-LD format.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class JsonLdResponse extends Response
{
    /**
     * @var boolean Include whitespaces to improve readability?
     */
    private static $pretty = true;

    /**
     * Pretty-print responses?
     *
     * If pretty-print is activated, whitespaces are included when
     * serializing the response to improve readability?
     *
     * @param boolean $pretty Include whitespaces to improve readability?
     */
    public static function setPretty($pretty)
    {
        self::$pretty = $pretty;
    }

    /**
     * Constructor
     *
     * @param mixed   $data    The response data
     * @param integer $status  The response status code
     * @param array   $headers An array of response headers
     */
    public function __construct($data = array(), $status = 200, $headers = array())
    {
        parent::__construct('', $status, $headers);

        // Only set the header when there is none
        // in order to not overwrite a custom definition.
        if (!$this->headers->has('Content-Type')) {
            $this->headers->set('Content-Type', 'application/ld+json');
        }

        $this->processData($data);
    }

    /**
     * {@inheritDoc}
     */
    public static function create($data = array(), $status = 200, $headers = array())
    {
        return new static($data, $status, $headers);
    }

    /**
     * Processes the data
     *
     * If necessary, the data is serialized and then set as the content of
     * the response.
     *
     * @param mixed $data
     *
     * @return JsonLdResponse
     */
    public function processData($data = array())
    {
        // return an empty object instead of an empty array
        if (is_array($data) && 0 === count($data)) {
            $data = new \stdClass();
        }

        if (!is_string($data)) {
            $options = 0;

            if (PHP_VERSION_ID >= 50400)
            {
                $options |= JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;

                if (self::$pretty)
                {
                    $options |= JSON_PRETTY_PRINT;
                }

                $data = json_encode($data, $options);
            }
            else
            {
                $data = json_encode($data);
                $data = str_replace('\\/', '/', $data);  // unescape slahes

                // unescape unicode
                $data = preg_replace_callback(
                    '/\\\\u([a-f0-9]{4})/',
                    function ($match) {
                        return iconv('UCS-4LE', 'UTF-8', pack('V', hexdec($match[1])));
                    },
                    $data);
            }
        }

        return $this->setContent($data);
    }
}


<?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;

/**
 * Hydra's default NamingStrategy
 *
 * Uses the classes name (without namespace) as both IRI fragment and short
 * name. The IRI fragment for properties has the "ClassName/propertyName"
 * form and the short name is the underscored, lowercases "property_name".
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com
 */
class DefaultNamingStrategy implements NamingStrategy
{
    /**
     * {@inheritdoc}
     */
    public function classIriFragment($className)
    {
        return $this->classShortName($className);
    }

    /**
     * {@inheritdoc}
     */
    public function propertyIriFragment($className, $propertyName)
    {
        $property = preg_replace_callback(
            '/_(.)/',
            function ($match) {
                return strtoupper($match[1]);
            },
            $propertyName
        );

        // Remove "get" and "set" prefixes
        if (0 === strncmp($property, 'get', 3) || 0 === strncmp($property, 'set', 3)) {
            $property = strtolower($property[3]) . substr($property, 4);

            // ... and the Iri suffix of such methods
            if ('Iri' === substr($property, -3)) {
                $property = substr($property, 0, -3);
            }
        }

        // The # is already included in the classIriFragment
        return $this->classIriFragment($className) . '/' . $property;
    }

    /**
     * {@inheritdoc}
     */
    public function classShortName($className)
    {
        if (strpos($className, '\\') !== false) {
            return substr($className, strrpos($className, '\\') + 1);
        }

        return $className;
    }

    /**
     * {@inheritdoc}
     */
    public function propertyShortName($className, $propertyName)
    {
        $property = strtolower(
            preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $propertyName)
        );

        // Remove "get" and "set" prefixes
        if (0 === strncmp($property, 'get_', 3) || 0 === strncmp($property, 'set_', 3)) {
            $property = substr($property, 4);

            // ... and the Iri suffix of such methods
            if ('_iri' === substr($property, -4)) {
                $property = substr($property, 0, -4);
            }
        }

        return $property;
    }
}


<?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\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Config\Resource\FileResource;

/**
 * HydraExtension
 *
 * Manages the dependency injection container configuration of the
 * HydraBundle.
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class HydraExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $config, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $config);

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');

        $this->loadMappingInformation($config, $container);
        $this->loadCacheDriver($config, $container);
    }

    /**
     * Loads mapping information
     *
     * There are two distinct configuration possibilities for mapping
     * information:
     *
     * 1. Specify a bundle and optionally details where the entity or
     *    mapping information reside.
     * 2. Specify an arbitrary mapping location.
     *
     * <code>
     *  hydra:
     *     mappings:
     *         MyBundle1: ~
     *         MyBundle2: yml
     *         MyBundle3: { type: annotation, dir: Entities/ }
     *         MyBundle4: { type: xml, dir: Resources/config/hydra/mapping }
     *         MyBundle5:
     *             type: yml
     *             dir: [ bundle-mappings1/, bundle-mappings2/ ]
     *         arbitrary_key:
     *             type: xml
     *             dir: %kernel.dir%/../src/vendor/ApiExtensions/config/
     *             prefix: ApiExtensions\Entities\
     * </code>
     *
     * In the case of bundles everything is really optional (the missing
     * parts can be auto-detected), otherwise everything is a required
     * argument.
     *
     * @param array            $config    An array of configuration settings
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @throws \InvalidArgumentException
     */
    protected function loadMappingInformation(array $config, ContainerBuilder $container)
    {
// FIXXME Remove $this->drivers if possible
        // reset state of drivers map. They are only used by this methods and children.
        $this->drivers = array();

        if ($config['auto_mapping']) {
            // automatically register bundle mappings
            foreach (array_keys($container->getParameter('kernel.bundles')) as $bundle) {
                if (!isset($config['mappings'][$bundle])) {
                    $config['mappings'][$bundle] = array(
                        'mapping'   => true,
                        'is_bundle' => true,
                    );
                }
            }
        }

        $container->setAlias('hydra.naming_strategy', new Alias($config['naming_strategy'], false));

        foreach ($config['mappings'] as $mappingName => $mappingConfig) {
            if (null !== $mappingConfig && false === $mappingConfig['mapping']) {
                continue;
            }

            $mappingConfig = array_replace(array(
                'dir'    => false,
                'type'   => false,
                'prefix' => false,
            ), (array) $mappingConfig);

            $mappingConfig['dir'] = $container->getParameterBag()->resolveValue($mappingConfig['dir']);

            // a bundle configuration is detected by realizing that the specified dir is not absolute and existing
            if (!isset($mappingConfig['is_bundle'])) {
                $mappingConfig['is_bundle'] = !is_dir($mappingConfig['dir']);
            }

            if ($mappingConfig['is_bundle']) {
                $bundle = null;
                foreach ($container->getParameter('kernel.bundles') as $name => $class) {
                    if ($mappingName === $name) {
                        $bundle = new \ReflectionClass($class);

                        break;
                    }
                }

                if (null === $bundle) {
                    throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled.', $mappingName));
                }

                $mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container);
                if (!$mappingConfig) {
                    continue;
                }
            }
            $this->validateMappingConfiguration($mappingConfig, $mappingName);
            $this->setMappingDriverConfig($mappingConfig, $mappingName);
        }

        $this->registerMappingDrivers($config, $container);
    }

    /**
     * If this is a bundle controlled mapping all the missing information can be autodetected by this method.
     *
     * Returns false when autodetection failed, an array of the completed information otherwise.
     *
     * @param array            $bundleConfig
     * @param \ReflectionClass $bundle
     * @param ContainerBuilder $container    A ContainerBuilder instance
     *
     * @return array|false
     */
    protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $container)
    {
        $bundleDir = dirname($bundle->getFilename());

        if (!$bundleConfig['type']) {
            $bundleConfig['type'] = $this->detectMetadataDriver($bundleDir, $container);
        }

        if (!$bundleConfig['type']) {
            // skip this bundle, no mapping information was found.
            return false;
        }

        if (!$bundleConfig['dir']) {
            if (in_array($bundleConfig['type'], array('annotation', 'staticphp'))) {
                $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingObjectDefaultName();
            } else {
                $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory();
            }
        } else {
            $bundleConfig['dir'] = $bundleDir.'/'.$bundleConfig['dir'];
        }

        if (!$bundleConfig['prefix']) {
            $bundleConfig['prefix'] = $bundle->getNamespaceName().'\\'.$this->getMappingObjectDefaultName();
        }

        return $bundleConfig;
    }

    /**
     * Detects what metadata driver to use for the supplied directory.
     *
     * @param string           $dir       A directory path
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @return string|null A metadata driver short name, if one can be detected
     */
    protected function detectMetadataDriver($dir, ContainerBuilder $container)
    {
        // add the closest existing directory as a resource
        $configPath = $this->getMappingResourceConfigDirectory();
        $resource = $dir.'/'.$configPath;
        while (!is_dir($resource)) {
            $resource = dirname($resource);
        }

        $container->addResource(new FileResource($resource));

        $extension = $this->getMappingResourceExtension();
        if (($files = glob($dir.'/'.$configPath.'/*.'.$extension.'.xml')) && count($files)) {
            return 'xml';
        } elseif (($files = glob($dir.'/'.$configPath.'/*.'.$extension.'.yml')) && count($files)) {
            return 'yml';
        } elseif (($files = glob($dir.'/'.$configPath.'/*.'.$extension.'.php')) && count($files)) {
            return 'php';
        }

        // add the directory itself as a resource
        $container->addResource(new FileResource($dir));

        if (is_dir($dir.'/'.$this->getMappingObjectDefaultName())) {
            return 'annotation';
        }

        return null;
    }

    /**
     * Noun that describes the mapped objects such as Entity or Controller.
     *
     * Will be used for auto-detection of persistent objects directory.
     *
     * @return string
     */
    protected function getMappingObjectDefaultName()
    {
        return 'Entity';
    }

    /**
     * Relative path from the bundle root to the directory where mapping files reside.
     *
     * @return string
     */
    protected function getMappingResourceConfigDirectory()
    {
        return 'Resources/config/hydra';
    }

    /**
     * Extension used by the mapping files.
     *
     * @return string
     */
    protected function getMappingResourceExtension()
    {
        return 'hydra';
    }

    /**
     * Register the mapping driver configuration for later use with the object managers metadata driver chain.
     *
     * @param array  $mappingConfig
     * @param string $mappingName
     *
     * @throws \InvalidArgumentException
     */
    protected function setMappingDriverConfig(array $mappingConfig, $mappingName)
    {
        if (is_dir($mappingConfig['dir'])) {
            $this->drivers[$mappingConfig['type']][$mappingConfig['prefix']] = realpath($mappingConfig['dir']);
        } else {
            throw new \InvalidArgumentException(sprintf('Invalid Hydra mapping path given. Cannot load Hydra mapping/bundle named "%s".', $mappingName));
        }
    }

    /**
     * Register all the collected mapping information with the object manager by registering the appropriate mapping drivers.
     *
     * @param array            $config    An array of configuration settings
     * @param ContainerBuilder $container A ContainerBuilder instance
     */
    protected function registerMappingDrivers($config, ContainerBuilder $container)
    {
        // configure metadata driver for each bundle based on the type of mapping files found
        if ($container->hasDefinition('hydra.metadata_driver')) {
            $chainDriverDef = $container->getDefinition('hydra.metadata_driver');
        } else {
            $chainDriverDef = new Definition('%hydra.metadata.driver_chain.class%');
            $chainDriverDef->setPublic(false);
        }

        foreach ($this->drivers as $driverType => $driverPaths) {
            $mappingService = 'hydra.'.$driverType.'_metadata_driver';
            if ($container->hasDefinition($mappingService)) {
                $mappingDriverDef = $container->getDefinition($mappingService);
                $args = $mappingDriverDef->getArguments();
                if ($driverType == 'annotation') {
                    $args[1] = array_merge(array_values($driverPaths), $args[1]);
                } else {
                    $args[0] = array_merge(array_values($driverPaths), $args[0]);
                }
                $mappingDriverDef->setArguments($args);
            } elseif ($driverType == 'annotation') {
                $mappingDriverDef = new Definition('%hydra.metadata.annotation.class%', array(
                    new Reference('hydra.metadata.annotation_reader'),
                    array_values($driverPaths),
                    new Reference('router')
                ));
            } else {
                $mappingDriverDef = new Definition('%hydra.metadata.'.$driverType.'.class%', array(
                    array_values($driverPaths)
                ));
            }
            $mappingDriverDef->setPublic(false);
            if (false !== strpos($mappingDriverDef->getClass(), 'yml') || false !== strpos($mappingDriverDef->getClass(), 'xml')) {
                $mappingDriverDef->setArguments(array(array_flip($driverPaths)));
                $mappingDriverDef->addMethodCall('setGlobalBasename', array('mapping'));
            }

            $container->setDefinition($mappingService, $mappingDriverDef);

            foreach ($driverPaths as $prefix => $driverPath) {
                $chainDriverDef->addMethodCall('addDriver', array(new Reference($mappingService), $prefix));
            }
        }

        $container->setDefinition('hydra.metadata_driver', $chainDriverDef);
    }

    /**
     * Validates the the specified mapping information
     *
     * @param array  $mappingConfig
     * @param string $mappingName
     *
     * @throws \InvalidArgumentException
     */
    protected function validateMappingConfiguration(array $mappingConfig, $mappingName)
    {
        if (!$mappingConfig['type'] || !$mappingConfig['dir'] || !$mappingConfig['prefix']) {
            throw new \InvalidArgumentException(
                sprintf('Hydra mapping definitions for "%s" require at least the "type", "dir" and "prefix" options.', $mappingName)
            );
        }

        if (!is_dir($mappingConfig['dir'])) {
            throw new \InvalidArgumentException(
                sprintf('Specified non-existing directory "%s" as Hydra mapping source.', $mappingConfig['dir'])
            );
        }

        if (!in_array($mappingConfig['type'], array('xml', 'yml', 'annotation', 'php', 'staticphp'))) {
            // FIXXME Make sure hydra.metadata_driver exists
            throw new \InvalidArgumentException(
                'Can only configure "xml", "yml", "annotation", "php" or '.
                '"staticphp" through the HydraBundle. Use your own bundle to configure other metadata drivers. '.
                'You can register them by adding a new driver to the '.
                '"hydra.metadata_driver" service definition.'
            );
        }
    }

    /**
     * Loads a metadata cache driver
     *
     * @param array            $config    An array of configuration settings
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @throws \InvalidArgumentException In case of unknown driver type.
     */
    protected function loadCacheDriver(array $config, ContainerBuilder $container)
    {
        $cacheDriver = $config['metadata_cache_driver'];
        $cacheDriverService = 'hydra.metadata_cache';

        switch ($cacheDriver['type']) {
            case 'service':
                $container->setAlias($cacheDriverService, new Alias($cacheDriver['id'], false));

                return;
            case 'file':
                $cacheDir = $container->getParameterBag()->resolveValue($cacheDriver['file_cache_dir']);

                if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true)) {
                    throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir));
                }

                $cacheDef = new Definition('%hydra.cache.file.class%', array($cacheDir, '.hydracache.php'));
                break;
            case 'memcache':
                $memcacheClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%hydra.cache.memcache.class%';
                $memcacheInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%hydra.cache.memcache_instance.class%';
                $memcacheHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%hydra.cache.memcache_host%';
                $memcachePort = !empty($cacheDriver['port']) || (isset($cacheDriver['port']) && $cacheDriver['port'] === 0)  ? $cacheDriver['port'] : '%hydra.cache.memcache_port%';
                $cacheDef = new Definition($memcacheClass);
                $memcacheInstance = new Definition($memcacheInstanceClass);
                $memcacheInstance->addMethodCall('connect', array(
                    $memcacheHost, $memcachePort
                ));
                $container->setDefinition(sprintf('hydra.%s_memcache_instance', $config['name']), $memcacheInstance);
                $cacheDef->addMethodCall('setMemcache', array(new Reference(sprintf('hydra.%s_memcache_instance', $config['name']))));
                break;
            case 'memcached':
                $memcachedClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%hydra.cache.memcached.class%';
                $memcachedInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%hydra.cache.memcached_instance.class%';
                $memcachedHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%hydra.cache.memcached_host%';
                $memcachedPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%hydra.cache.memcached_porthydra.%';
                $cacheDef = new Definition($memcachedClass);
                $memcachedInstance = new Definition($memcachedInstanceClass);
                $memcachedInstance->addMethodCall('addServer', array(
                    $memcachedHost, $memcachedPort
                ));
                $container->setDefinition(sprintf('hydra.%s_memcached_instance', $config['name']), $memcachedInstance);
                $cacheDef->addMethodCall('setMemcached', array(new Reference(sprintf('hydra.%s_memcached_instance', $config['name']))));
                break;
            case 'redis':
                $redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%hydra.cache.redis.class%';
                $redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%hydra.cache.redis_instance.class%';
                $redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%hydra.cache.redis_host%';
                $redisPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%hydra.cache.redis_port%';
                $cacheDef = new Definition($redisClass);
                $redisInstance = new Definition($redisInstanceClass);
                $redisInstance->addMethodCall('connect', array(
                    $redisHost, $redisPort
                ));
                $container->setDefinition(sprintf('hydra.%s_redis_instance', $config['name']), $redisInstance);
                $cacheDef->addMethodCall('setRedis', array(new Reference(sprintf('hydra.%s_redis_instance', $config['name']))));
                break;
            case 'apc':
            case 'array':
            case 'xcache':
            case 'wincache':
            case 'zenddata':
                $cacheDef = new Definition('%hydra.'.sprintf('cache.%s.class', $cacheDriver['type']).'%');
                break;
            default:
                throw new \InvalidArgumentException(sprintf('"%s" is an unrecognized Hydra cache driver.', $cacheDriver['type']));
        }

        $cacheDef->setPublic(false);
        // generate a unique namespace for the given application
        $namespace = 'sf2'.$this->getMappingResourceExtension().md5($container->getParameter('kernel.root_dir').$container->getParameter('kernel.environment'));
        $cacheDef->addMethodCall('setNamespace', array($namespace));

        $container->setDefinition($cacheDriverService, $cacheDef);
    }
}


<?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;

use Symfony\Component\Routing\RouterInterface;
use ML\HydraBundle\Mapping\ClassMetadataFactory;

/**
 * A Hydra Web API
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class HydraApi
{
    /**
     * @var \Symfony\Component\Routing\RouterInterface The router
     */
    private $router;

    /**
     * @var string The vocabulary's absolute URL
     */
    private $vocabUrl;

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

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

    /**
     * @var string The primary entry point of the API
     */
    private $entrypoint;

    /**
     * @var array Additional information about status codes that might be
     *            returned by the API
     */
    private $statusCodes;

    /**
     * @var \ML\HydraBundle\Mapping\ClassMetadataFactory
     */
    private $metadata;

    /**
     * @var array Mappings from scalar types to the corresponding XSD datatypes
     */
    private static $typeMap = array(
        'string' => 'http://www.w3.org/2001/XMLSchema#string',
        'integer' => 'http://www.w3.org/2001/XMLSchema#integer',
        'float' => 'http://www.w3.org/2001/XMLSchema#double',
        'double' => 'http://www.w3.org/2001/XMLSchema#double',
        'boolean' => 'http://www.w3.org/2001/XMLSchema#boolean',
        'bool' => 'http://www.w3.org/2001/XMLSchema#boolean',
        'void' => 'http://www.w3.org/2002/07/owl#Nothing',
        'mixed' => null
    );

    /**
     * Constructor
     *
     * @param RouterInterface      $router          The router.
     * @param ClassMetadataFactory $metadataFactory The metadata factory.
     * @param string               $vocabRoute      The route to use for
     *                                              vocabulary definitions.
     */
    public function __construct(RouterInterface $router, ClassMetadataFactory $metadataFactory, $normalizers, $vocabRoute = 'hydra_vocab')
    {
        $this->router = $router;
        $this->metadata = $metadataFactory;
        $this->normalizers = $normalizers;
        $this->vocabUrl = $this->router->generate($vocabRoute, array(), true);
    }

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

    /**
     * Exists a normalizer for the passed class?
     *
     * @param string $class The class for which a normalizer is required.
     *
     * @return boolean Returns true if a normalizer exists, false otherwise.
     */
    public function hasNormalizer($class)
    {
        if ('\\' === $class[0]) {
            $class = substr($class, 1);
        }

        return array_key_exists($class, $this->normalizers);
    }

    /**
     * Gets the normalizer for the passed class
     *
     * @param string $class The class whose normalizer should be retrieved
     *
     * @return object Returns the normalizer for the specified class
     */
    public function getNormalizer($class)
    {
        if ('\\' === $class[0]) {
            $class = substr($class, 1);
        }

        return $this->normalizers[$class];
    }

    /**
     * Gets the context corresponding to the passed class
     *
     * @param  string $exposedClassName The exposed class name
     *
     * @return array|null The context in the form of an associative array or
     *                    null if no class is exposed with the specified
     *                    name.
     */
    public function getContext($exposedClassName)
    {
        $classes = $this->metadata->getAllMetadata();
        $metadata = null;

        foreach ($classes as $class) {
            if ($class->getExposeAs() === $exposedClassName) {
                $metadata = $class;
                break;
            }
        }

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

        $context = array(
            'hydra' => 'http://www.w3.org/ns/hydra/core#',
            'vocab' => $this->vocabUrl . '#'
        );

        $context[$exposedClassName] = ($metadata->isExternalReference())
            ? $metadata->getIri()
            : 'vocab:' . $metadata->getIri();

        foreach ($metadata->getProperties() as $property) {
            // If something is exposed as keyword, no context definition is necessary
            if (0 === strncmp($property->getExposeAs(), '@', 1)) {
                // TODO Make this check more reliable by just checking for actual keywords
                //      What should we do if we serialize to RDFa for instance?
                continue;
            }

            $termDefinition = ($property->isExternalReference())
                ? $property->getIri()
                : 'vocab:' . $property->getIri();

            // TODO Make this check more robust
            if ($property->getRoute()) {
                $termDefinition = array('@id' => $termDefinition, '@type' => '@id');
            } elseif ($this->hasNormalizer($property->getType())) {
                $normalizer = $this->getNormalizer($property->getType());
                $termDefinition = array('@id' => $termDefinition, '@type' => $normalizer->getTypeIri());
            }

            $context[$property->getExposeAs()] = $termDefinition;
        }

        return array('@context' => $context);
    }

    /**
     * Get the URL of the Hydra ApiDocumentation
     *
     * @return string The URL of the Hydra ApiDocumentation
     */
    public function getDocumentationUrl()
    {
        return $this->vocabUrl;
    }

    /**
     * Get the Hydra ApiDocumentation
     *
     * @return array The Hydra ApiDocumentation in the form of an array
     *               ready to be serialized to JSON-LD
     */
    public function getDocumentation()
    {
        $metadata = $this->metadata->getAllMetadata();

        $docu = array(
            '@context' => array(
                'vocab' => $this->vocabUrl . '#',
                'hydra' => 'http://www.w3.org/ns/hydra/core#',
                'ApiDocumentation' => 'hydra:ApiDocumentation',
                'property' => array('@id' => 'hydra:property', '@type' => '@id'),
                'readonly' => 'hydra:readonly',
                'writeonly' => 'hydra:writeonly',
                'supportedClass' => 'hydra:supportedClass',
                'supportedProperty' => 'hydra:supportedProperty',
                'supportedOperation' => 'hydra:supportedOperation',
                'method' => 'hydra:method',
                'expects' =>  array('@id' => 'hydra:expects', '@type' => '@id'),
                'returns' =>  array('@id' => 'hydra:returns', '@type' => '@id'),
                'statusCodes' => 'hydra:statusCodes',
                'code' => 'hydra:statusCode',
                'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
                'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#',
                'label' => 'rdfs:label',
                'description' => 'rdfs:comment',
                'domain' => array('@id' => 'rdfs:domain', '@type' => '@id'),
                'range' =>  array('@id' => 'rdfs:range', '@type' => '@id'),
                'subClassOf' =>  array('@id' => 'rdfs:subClassOf', '@type' => '@id'),
            ),
            '@id' => $this->vocabUrl,
            '@type' => 'ApiDocumentation',
            'supportedClass' => array()
        );

        foreach ($metadata as $class) {
            if ($class->isExternalReference()) {
                $docu['supportedClass'][] = array(
                    '@id' => $class->getIri(),
                    '@type' => 'hydra:Class',
                    'hydra:title' => $class->getTitle(),
                    'hydra:description' => $class->getDescription(),
                    'supportedOperation' => $this->documentOperations($class->getOperations()),
                    'supportedProperty' => $this->documentClassProperties($class),
                );
            } else {
                if (false !== ($superclass = get_parent_class($class->getName()))) {
                    try {
                        $superclass = $this->metadata->getMetadataFor($superclass);

                        $superclass = $superclass->isExternalReference()
                            ? $superclass->getIri()
                            : 'vocab:' . $superclass->getIri();
                    } catch (\Exception $e) {
                        $superclass = null;
                    }
                } else {
                    $superclass = null;
                }

                $docu['supportedClass'][] = array(
                    '@id' => 'vocab:' . $class->getIri(),
                    '@type' => 'hydra:Class',
                    'subClassOf' => $superclass,
                    'label' => $class->getTitle(),
                    'description' => $class->getDescription(),
                    'supportedOperation' => $this->documentOperations($class->getOperations()),
                    'supportedProperty' => $this->documentClassProperties($class),
                );
            }

        }

        return $docu;
    }

    /**
     * Creates a JSON-LD serialization of the passed operations
     *
     * @param array|null $operations The operations
     *
     * @return array|null The JSON-LD serialization of the operations
     */
    private function documentOperations($operations)
    {
        if (null === $operations) {
            return null;
        }

        $result = array();

        foreach ($operations as $operation) {
            $statusCodes = array();
            foreach ($operation->getStatusCodes() as $code => $description) {
                $statusCodes[] = array(
                    'code' => $code,
                    'description' => $description
                );
            }

            $result[] = array(
                '@id' => '_:' . $operation->getName(),
                '@type' => $operation->getType() ?: 'hydra:Operation',
                'method' => $operation->getMethod(),
                'label' => ($operation->getTitle())
                    ?
                    : $operation->getDescription(),
                'description' => (null === $operation->getTitle())
                    ? null
                    : $operation->getDescription(),
                'expects' => $this->getTypeReferenceIri($operation->getExpects()),
                'returns' => $this->getTypeReferenceIri($operation->getReturns()),
                'statusCodes' => $statusCodes
            );
        }

        return $result;
    }

    /**
     * Documents the properties of the passed ClassMetadata object
     *
     * @param ML\HydraBundle\Mapping\ClassMetadata $class The class metadata
     *
     * @return array The JSON-LD serialization of the class' properties.
     */
    private function documentClassProperties(\ML\HydraBundle\Mapping\ClassMetadata $class)
    {
        $result = array();
        $propertyDomain = $this->getTypeReferenceIri($class->getName());

        foreach ($class->getProperties() as $property) {
            if (0 === strncmp('@', $property->getExposeAs(), 1)) {
                continue;  // ignore properties that are mapped to keywords
            }

            $result[] = array(
                'property' => ($property->isExternalReference())
                    ? $property->getIri()
                    : array(
                        '@id' => 'vocab:' . $property->getIri(),
                        '@type' => ($property->getRoute())
                            ? 'hydra:Link'
                            : 'rdf:Property',
                        'label' => $property->getTitle(),
                        'description' => $property->getDescription(),
                        'domain' => $propertyDomain,
                        'range' => $this->getTypeReferenceIri($property->getType()),
                        'supportedOperation' => $this->documentOperations($property->getOperations())
                    ),
                'hydra:title' => $property->getTitle(),
                'hydra:description' => $property->getDescription(),
                'required' => $property->getRequired(),
                'readonly' => $property->isReadOnly(),
                'writeonly' => $property->isWriteOnly()
            );
        }

        return $result;
    }

    /**
     * Get the (compact) IRI to reference the specified type
     *
     * @param string|null $type The type to reference.
     *
     * @return string|null The IRI corresponding to the type.
     */
    private function getTypeReferenceIri($type)
    {
        if (null === $type) {
            return null;
        }

        if (array_key_exists($type, self::$typeMap)) {
            return self::$typeMap[$type];
        }

        if ($this->hasNormalizer($type)) {
            return $this->getNormalizer($type)->getTypeIri();
        }

        $metadata = $this->metadata->getMetadataFor($type);

        if (null === $metadata) {
            // TODO Improve this
            throw new \Exception('Found invalid type: ' . $type);
        }

        if ($metadata->isExternalReference()) {
            return $metadata->getIri();
        } else {
            return 'vocab:' . $metadata->getIri();
        }
    }
}


<?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;

/**
 * Expose annotation
 *
 * The Expose annotation defines whether an element is exposed by the
 * serializer (or set by the deserializer). If an element is being exposed,
 * it is possible to define it's short name with the "as" parameter and it's
 * IRI (fragment) with the "iri" parameter. If the value of "iri" contains
 * a colon, it is assumed to represent the full, absolute IRI, otherwise it
 * is interpreted as IRI fragment which is appended to the vocabulary's IRI.
 * The three parameters "required", "readonly" and "writeonly" are used only
 * for properties.
 *
 * @Annotation
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class Expose
{
    /**
     * @var string
     */
    public $as = null;

    /**
     * @var string
     */
    public $iri = null;

    /**
     * @var boolean
     */
    public $required;

    /**
     * @var boolean
     */
    public $readonly;

    /**
     * @var boolean
     */
    public $writeonly;

    /**
     * Get the IRI (fragment)
     *
     * @return string The IRI fragment
     */
    public function getIri()
    {
        if (null === $this->iri) {
            return null;
        }

        // Is it an absolute IRI?
        if (false !== strpos($this->iri, ':')) {
            return $this->iri;
        }

        if ('#' === $this->iri[0]) {
            return substr($this->iri, 1);
        }

        return $this->iri;
    }
}


<?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 Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use ML\HydraBundle\JsonLdResponse;

/**
 * The documentation generator controller
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class DocumentationGeneratorController extends Controller
{
    /**
     * Generates the service documentation
     *
     * @Route("/api-documentation/")
     * @Template()
     */
    public function indexAction()
    {
        $documentation = $this->get('hydra.api')->getDocumentation();

        return $documentation;
    }

    /**
     * Generates the service documentation
     *
     * @Route("/vocab", defaults = { "_format" = "jsonld" }, name="hydra_vocab")
     * @Template()
     */
    public function vocabularyAction()
    {
        $hydra = $this->get('hydra.api');
        $documentation = $hydra->getDocumentation();

        return new JsonLdResponse($documentation);
    }

    /**
     * Generates the service documentation
     *
     * @Route("/contexts/{type}.jsonld", defaults = { "_format" = "jsonld" }, name="hydra_context")
     */
    public function getContextAction($type)
    {
        $context = $this->get('hydra.api')->getContext($type);

        if (null === $context) {
            $this->createNotFoundException();
        }

        return new JsonLdResponse($context);
    }
}


<?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 ML\HydraBundle\Mapping\ClassMetadata;

/**
 * DriverChain
 *
 * @author Markus Lanthaler <mail@markus-lanthaler.com>
 */
class DriverChain
{
    /**
     * @var array
     */
    private $drivers = array();

    /**
     * Add a nested driver.
     *
     * @param MappingDriver $nestedDriver
     * @param string $namespace
     */
    public function addDriver(MappingDriver $nestedDriver, $namespace)
    {
        $this->drivers[$namespace] = $nestedDriver;
    }

    /**
     * Get the array of nested drivers.
     *
     * @return array $drivers
     */
    public function getDrivers()
    {
        return $this->drivers;
    }

    /**
     * Loads the metadata for the specified class
     *
     * @param string $className The name of the class.
     *
     * @return ClassMetadata|null The metadata for the specified class or null.
     */
    public function loadMetadataForClass($className)
    {
        foreach ($this->drivers as $namespace => $driver) {
            if (strpos($className, $namespace) === 0) {
                return $driver->loadMetadataForClass($className);
            }
        }

        return null;
    }

    /**
     * Gets the names of all mapped classes known to this driver.
     *
     * @return array The names of all mapped classes known to this driver.
     */
    public function getAllClassNames()
    {
        $classNames = array();
        $driverClasses = array();

        foreach ($this->drivers as $namespace => $driver) {
            $oid = spl_object_hash($driver);

            if (!isset($driverClasses[$oid])) {
                $driverClasses[$oid] = $driver->getAllClassNames();
            }

            foreach ($driverClasses[$oid] as $className) {
                if (strpos($className, $namespace) === 0) {
                    $classNames[$className] = true;
                }
            }
        }

        return array_keys($classNames);
    }
}


<?php

namespace yii2mod\timezone;

use Yii;
use yii\base\Component;
use yii\web\Controller;
use yii\helpers\Url;

/**
 * Class Timezone
 * @package yii2mod\timezone
 */
class Timezone extends Component
{
    /**
     * @var string
     */
    public $actionRoute = '/site/timezone';

    /**
     * @var timezone name (ex: Europe/Kiev)
     */
    public $name;

    /**
     * Registering offset-getter if timezone is not set
     */
    public function init()
    {
        $this->actionRoute = Url::toRoute($this->actionRoute);
        $this->name = Yii::$app->session->get('timezone');
        if ($this->name == null) {
            $this->registerTimezoneScript($this->actionRoute);
            $this->name = date_default_timezone_get();
        }
        Yii::$app->setTimeZone($this->name);
    }

    /**
     * Registering script for timezone detection on before action event
     * @param $actionRoute
     */
    public function registerTimezoneScript($actionRoute)
    {
        Yii::$app->on(Controller::EVENT_BEFORE_ACTION, function ($event) use ($actionRoute) {
            $view = $event->sender->view;
            $js = <<<JS
                var timezone = '';
                var timezoneAbbr = '';
                try {
                    var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
                    var timezoneAbbr = /\((.*)\)/.exec(new Date().toString())[1];
                }
                catch(err) {
                    console.log(err);
                }
                $.post("$actionRoute", {
                    timezone: timezone,
                    timezoneAbbr: timezoneAbbr,
                    timezoneOffset: -new Date().getTimezoneOffset() / 60
                });
JS;
            $view->registerJs($js);
        });
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for KNAB documents
 *
 * @author Casper Bakker <github@casperbakker.com>
 */
class Knab extends AbstractParser
{

    /**
     * Test if the document is an KNAB document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return strpos(strtok($text, "\n"), 'KNABNL') !== false;
    }

    /**
     * Get the closing balance
     *
     * @param mixed $text
     * @return void
     */
    protected function closingBalance($text)
    {
        if ($line = $this->getLine('62M|62F', $text)) {
            return $this->balance($this->reader->createClosingBalance(), $line);
        }
    }

    /**
     * Get the contra account number from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        foreach ($lines as $line) {
            if (preg_match('/REK\: ([a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16})/', $line, $match)) {
                return rtrim(ltrim($match[1], '0P'));
            }
        }
    }

    protected function contraAccountName(array $lines)
    {
        foreach ($lines as $line) {
            if (preg_match('/NAAM: (.+)/', $line, $match)) {
                return trim($match[1]);
            }
        }
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940;

/**
 * Read and parse MT940 documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class Reader
{
    // Properties {{{

    /**
     * @var array A class map of bank parsers
     */
    private $parsers = array();

    /**
     * @var array All the parsers shipped in this package
     */
    private $defaultParsers = array(
        'ABN-AMRO'    => 'Jejik\MT940\Parser\AbnAmro',
        'ING'         => 'Jejik\MT940\Parser\Ing',
        'Knab'        => 'Jejik\MT940\Parser\Knab',
        'PostFinance' => 'Jejik\MT940\Parser\PostFinance',
        'Rabobank'    => 'Jejik\MT940\Parser\Rabobank',
        'Sns'         => 'Jejik\MT940\Parser\Sns',
        'Triodos'     => 'Jejik\MT940\Parser\Triodos'
    );

    /**
     * @see setStatementClass()
     * @var string|callable
     */
    private $statementClass = 'Jejik\MT940\Statement';

    /**
     * @see setAccountClass()
     * @var string|callable
     */
    private $accountClass = 'Jejik\MT940\Account';

    /**
     * @see setContraAccountClass()
     * @var string|callable
     */
    private $contraAccountClass = 'Jejik\MT940\Account';

    /**
     * @see setTransactionClass()
     * @var string|callable
     */
    private $transactionClass = 'Jejik\MT940\Transaction';

    /**
     * @see setOpeningBalanceClass()
     * @var string|callable
     */
    private $openingBalanceClass = 'Jejik\MT940\Balance';

    /**
     * @see setClosingBalanceClass()
     * @var string|callable
     */
    private $closingBalanceClass = 'Jejik\MT940\Balance';

    // }}}

    // Parser management {{{

    /**
     * Get a list of default parsers shippen in this package
     *
     * @return array
     */
    public function getDefaultParsers()
    {
        return $this->defaultParsers;
    }

    /**
     * Get the current list of parsers
     *
     * @return array
     */
    public function getParsers()
    {
        return $this->parsers;
    }

    /**
     * Add a parser type to the list of parsers
     *
     * Some parsers can conflict with each other so order is important. Use
     * the $before parameter in insert a parser in a specific place.
     *
     * @param string $name Name of the parser
     * @param mixed $class Classname of the parser
     * @param mixed $before Insert the new parser before this parser
     * @return $this
     * @throws \RuntimeException if the $before parser does not exist
     */
    public function addParser($name, $class, $before = null)
    {
        if ($before === null) {
            $this->parsers[$name] = $class;
            return $this;
        }

        if (($offset = array_search($before, array_keys($this->parsers))) !== false) {
            $this->parsers = array_slice($this->parsers, 0, $offset, true)
                + array($name => $class)
                + array_slice($this->parsers, $offset, null, true);

            return $this;
        }

        throw new \RuntimeException(sprintf('Parser "%s" does not exist.', $before));
    }

    /**
     * Add multiple parsers in one step
     *
     * @param array $parsers Associative array of parser names and classes
     * @return $this
     */
    public function addParsers($parsers)
    {
        foreach ($parsers as $name => $class) {
            $this->addParser($name, $class);
        }

        return $this;
    }

    /**
     * Remove a parser
     *
     * @param string $name Parser to remove
     * @return $this
     */
    public function removeParser($name)
    {
        unset($this->parsers[$name]);
    }

    /**
     * Set the list of parsers
     *
     * @param array $parsers Associative array of 'name' => 'class'
     * @return $this
     */
    public function setParsers(array $parsers = array())
    {
        $this->parsers = $parsers;
        return $this;
    }

    // }}}

    // Class factories {{{

    /**
     * Getter for statementClass
     *
     * @return string|callable
     */
    public function getStatementClass()
    {
        return $this->statementClass;
    }

    /**
     * Set the classname of the statement class or callable that returns an object that
     * implements the StatementInterface.
     *
     * The callable is passed the account object and statement sequence number
     * as parameters. Example:
     *
     * $reader->setStatementClass(function (AccountInterface $account, $number) {
     *     return new My\Statement();
     * });
     *
     * If the callable returns null, the statement is skipped.
     *
     * @param string|callable $statementClass
     * @return $this
     */
    public function setStatementClass($statementClass)
    {
        if (!is_callable($statementClass) && !class_exists($statementClass)) {
            throw new \InvalidArgumentException('$statementClass must be a valid classname or a PHP callable');
        }

        $this->statementClass = $statementClass;
        return $this;
    }

    /**
     * Create a Statement object
     *
     * @param string $account Account number
     * @param string $number Statement sequence number
     * @return StatementInterface
     */
    public function createStatement(AccountInterface $account, $number)
    {
        return $this->createObject($this->statementClass, 'Jejik\MT940\StatementInterface', array($account, $number));
    }

    /**
     * Getter for accountClass
     *
     * @return string|callable
     */
    public function getAccountClass()
    {
        return $this->accountClass;
    }

    /**
     * Set the classname of the account class or callable that returns an object that
     * implements the AccountInterface.
     *
     * The callable is passed the account number as a parameter. Example:
     *
     * $reader->setAccountClass(function ($accountNumber) {
     *     return new My\Account();
     * });
     *
     * If the callable returns null, statements for the account will be skipped.
     *
     * @param string|callable $accountClass
     * @return $this
     */
    public function setAccountClass($accountClass)
    {
        if (!is_callable($accountClass) && !class_exists($accountClass)) {
            throw new \InvalidArgumentException('$accountClass must be a valid classname or a PHP callable');
        }

        $this->accountClass = $accountClass;
        return $this;
    }

    /**
     * Create a Account object
     *
     * @param string $accountNumber Account number
     * @return AccountInterface
     */
    public function createAccount($accountNumber)
    {
        return $this->createObject($this->accountClass, 'Jejik\MT940\AccountInterface', array($accountNumber));
    }

    /**
     * Getter for contraAccountClass
     *
     * @return string|callable
     */
    public function getContraAccountClass()
    {
        return $this->contraAccountClass;
    }

    /**
     * Set the classname of the contraAccount class or callable that returns an object that
     * implements the AccountInterface.
     *
     * The callable is passed the account number as a parameter. Example:
     *
     * $reader->setContraAccountClass(function ($accountNumber) {
     *     return new My\ContraAccount();
     * });
     *
     * @param string|callable $contraAccountClass
     * @return $this
     */
    public function setContraAccountClass($contraAccountClass)
    {
        if (!is_callable($contraAccountClass) && !class_exists($contraAccountClass)) {
            throw new \InvalidArgumentException('$contraAccountClass must be a valid classname or a PHP callable');
        }

        $this->contraAccountClass = $contraAccountClass;
        return $this;
    }

    /**
     * Create a ContraAccount object
     *
     * @param string $accountNumber Contra account number
     * @return AccountInterface
     */
    public function createContraAccount($accountNumber)
    {
        return $this->createObject($this->contraAccountClass, 'Jejik\MT940\AccountInterface', array($accountNumber));
    }

    /**
     * Getter for transactionClass
     *
     * @return string|callable
     */
    public function getTransactionClass()
    {
        return $this->transactionClass;
    }

    /**
     * Set the classname of the transaction class or callable that returns an object that
     * implements the StatementInterface.
     *
     * The callable is not passed any arguments.
     *
     * $reader->setTransactionClass(function () {
     *     return new My\Transaction();
     * });
     *
     * @param string|callable $transactionClass
     * @return $this
     */
    public function setTransactionClass($transactionClass)
    {
        if (!is_callable($transactionClass) && !class_exists($transactionClass)) {
            throw new \InvalidArgumentException('$transactionClass must be a valid classname or a PHP callable');
        }

        $this->transactionClass = $transactionClass;
        return $this;
    }

    /**
     * Create a Transaction object
     *
     * @return TransactionInterface
     */
    public function createTransaction()
    {
        return $this->createObject($this->transactionClass, 'Jejik\MT940\TransactionInterface');
    }

    /**
     * Getter for openingBalanceClass
     *
     * @return string|callable
     */
    public function getOpeningBalanceClass()
    {
        return $this->openingBalanceClass;
    }

    /**
     * Set the classname of the opening balance class or callable that returns an object that
     * implements the BalanceInterface.
     *
     * The callable is not passed any arguments.
     *
     * $reader->setOpeningBalanceClass(function () {
     *     return new My\Balance();
     * });
     *
     * @param string|callable $openingBalanceClass
     * @return $this
     */
    public function setOpeningBalanceClass($openingBalanceClass)
    {
        if (!is_callable($openingBalanceClass) && !class_exists($openingBalanceClass)) {
            throw new \InvalidArgumentException('$openingBalanceClass must be a valid classname or a PHP callable');
        }

        $this->openingBalanceClass = $openingBalanceClass;
        return $this;
    }

    /**
     * Create an opening balance object
     *
     * @return BalanceInterface
     */
    public function createOpeningBalance()
    {
        return $this->createObject($this->openingBalanceClass, 'Jejik\MT940\BalanceInterface');
    }

    /**
     * Getter for closingBalanceClass
     *
     * @return string|callable
     */
    public function getClosingBalanceClass()
    {
        return $this->closingBalanceClass;
    }

    /**
     * Set the classname of the closing balance class or callable that returns an object that
     * implements the BalanceInterface.
     *
     * The callable is not passed any arguments.
     *
     * $reader->setClosingBalanceClass(function () {
     *     return new My\Balance();
     * });
     *
     * @param string|callable $closingBalanceClass
     * @return $this
     */
    public function setClosingBalanceClass($closingBalanceClass)
    {
        if (!is_callable($closingBalanceClass) && !class_exists($closingBalanceClass)) {
            throw new \InvalidArgumentException('$closingBalanceClass must be a valid classname or a PHP callable');
        }

        $this->closingBalanceClass = $closingBalanceClass;
        return $this;
    }

    /**
     * Create an closing balance object
     *
     * @return BalanceInterface
     */
    public function createClosingBalance()
    {
        return $this->createObject($this->closingBalanceClass, 'Jejik\MT940\BalanceInterface');
    }

    /**
     * Create an object of a specified interface
     *
     * @param string|callable $className Classname or a callable that returns an object instance
     * @param string $interface The interface the class must implement
     * @param array $params Parameters to pass to the callable
     *
     * @return object An object that implements the interface
     */
    protected function createObject($className, $interface, $params = array())
    {
        if (is_string($className) && class_exists($className)) {
            $object = new $className();
        } elseif (is_callable($className)) {
            $object = call_user_func_array($className, $params);
        } else {
            throw new \InvalidArgumentException('$className must be a valid classname or a PHP callable');
        }

        if (null !== $object && !($object instanceof $interface)) {
            throw new \InvalidArgumentException(sprintf('%s must implement %s', get_class($object), $interface));
        }

        return $object;
    }

    // }}}

    /**
     * Get MT940 statements from the input text
     *
     * @param string $text
     * @return \Jejik\MT940\Statement[]
     * @throws \RuntimeException if no suitable parser is found
     */
    public function getStatements($text)
    {
        if (!$this->parsers) {
            $this->addParsers($this->getDefaultParsers());
        }

        foreach ($this->parsers as $class) {
            $parser = new $class($this);
            if ($parser->accept($text)) {
                return $parser->parse($text);
            }
        }

        throw new \RuntimeException('No suitable parser found.');
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for ING documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class Ing extends AbstractParser
{
    /**
     * Test if the document is an ING document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return substr($text, 6, 6) === 'INGBNL';
    }

    /**
     * Parse a statement number
     *
     * @param string $text Statement body text
     * @return string|null
     */
    protected function statementNumber($text)
    {
        if ($number = $this->getLine('28C', $text)) {
            return $number;
        }

        return null;
    }

    /**
     * Create a Transaction from MT940 transaction text lines
     *
     * ING only provides a book date, not a valuation date. This
     * is opposite from standard MT940 so the AbstractReader will read it
     * as a valueDate. This must be corrected.
     *
     * ING does sometimes supplies a book date inside the description.
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return \Jejik\MT940\Transaction
     */
    protected function transaction(array $lines)
    {
        $transaction = parent::transaction($lines);
        $transaction->setBookDate($transaction->getValueDate())
                    ->setValueDate(null);

        if (preg_match('/transactiedatum: (\d{2}-\d{2}-\d{4})/', $lines[1], $match)) {
            $valueDate = \DateTime::createFromFormat('d-m-Y', $match[1]);
            $valueDate->setTime(0, 0, 0);

            $transaction->setValueDate($valueDate);
        }

        return $transaction;
    }

    /**
     * Get the contra account from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        if (preg_match('/^([0-9]{9,10}) /', $lines[1], $match)) {
            return $match[1];
        }

        return null;
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for SNS documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class Sns extends AbstractParser
{
    /**
     * Test if the document is an SNS document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return substr($text, 6, 8) === 'SNSBNL2A';
    }

    /**
     * Get the contra account from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        if (!isset($lines[1])) {
            return null;
        }

        if (preg_match('/^([0-9]{10}) /', $lines[1], $match)) {
            return ltrim($match[1], '0');
        }

        return null;
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for Triodos documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class Triodos extends AbstractParser
{
    /**
     * Test if the document is an ABN-AMRO document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return strpos($text, ':25:TRIODOSBANK') !== false;
    }

    /**
     * Parse a account number
     *
     * Remove the TRIODOSBANK/ prefix
     *
     * @param string $text Statement body text
     * @return string|null
     */
    protected function accountNumber($text)
    {
        if ($account = $this->getLine('25', $text)) {
            return ltrim(substr($account, 12), '0');
        }

        return null;
    }

    /**
     * Get the contra account from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        if (preg_match('/^000>1([0-9]{11})/', $lines[1], $match)) {
            return ltrim($match[1], '0');
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    protected function description($description)
    {
        return preg_replace('/>2[0-7]{1}/', '', $description);
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for ABN-AMRO documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class AbnAmro extends AbstractParser
{
    /**
     * Test if the document is an ABN-AMRO document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return substr($text, 0, 6) === 'ABNANL';
    }

    /**
     * Get the contra account from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        if (!isset($lines[1])) {
            return null;
        }

        if (preg_match('/^([0-9.]{11,14}) /', $lines[1], $match)) {
            return str_replace('.', '', $match[1]);
        }

        if (preg_match('/^GIRO([0-9 ]{9}) /', $lines[1], $match)) {
            return trim($match[1]);
        }

        return null;
    }

    /**
     * Get the contra account holder name from a transaction
     *
     * There is only a countra account name if there is a contra account number
     * The name immediately follows the number in the first 32 characters of the first line
     * If the charaters up to the 32nd after the number are blank, the name is found in
     * the rest of the line.
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountName(array $lines)
    {
        if (!isset($lines[1])) {
            return null;
        }

        $line = strstr($lines[1], "\r\n", true) ?: $lines[1];
        $offset = 0;

        if (preg_match('/^([0-9.]{11,14}) (.*)$/', $line, $match, PREG_OFFSET_CAPTURE)) {
            $offset = $match[2][1];
        }

        if (preg_match('/^GIRO([0-9 ]{9}) (.*)$/', $line, $match, PREG_OFFSET_CAPTURE)) {
            $offset = $match[2][1];
        }

        // No account number found, so no name either
        if (!$offset) {
            return null;
        }

        // Name in the first 32 characters
        if ($name = trim(substr($line, $offset, 32 - $offset))) {
            return $name;
        }

        // Name in the second 32 characters
        if ($name = trim(substr($line, 32, 32))) {
            return $name;
        }

        return null;
    }
}


<?php

/*
 * This file is part of the Jejik\MT940 library
 *
 * Copyright (c) 2012 Sander Marechal <s.marechal@jejik.com>
 * Licensed under the MIT license
 *
 * For the full copyright and license information, please see the LICENSE
 * file that was distributed with this source code.
 */

namespace Jejik\MT940\Parser;

/**
 * Parser for Rabobank documents
 *
 * @author Sander Marechal <s.marechal@jejik.com>
 */
class Rabobank extends AbstractParser
{
    const FORMAT_CLASSIC = 1;
    const FORMAT_STRUCTURED = 2;

    /**
     * @var int Document format
     */
    private $format = self::FORMAT_CLASSIC;

    /**
     * Test if the document is an ING document
     *
     * @param string $text
     * @return bool
     */
    public function accept($text)
    {
        return substr($text, 0, 5) === ':940:';
    }

    /**
     * Determine the format for this statement
     *
     * @param string $text Statement body text
     * @return \Jejik\MT940\Statement
     */
    protected function statementBody($text)
    {
        switch (substr($this->getLine('20', $text), 0, 4)) {
            case '940A':
                $this->format = self::FORMAT_CLASSIC;
                break;
            case '940S':
                $this->format = self::FORMAT_STRUCTURED;
                break;
            default:
                throw new \RuntimeException('Unknown file format');
        }

        return parent::statementBody($text);
    }

    /**
     * Parse an account number
     *
     * @param string $text Statement body text
     * @return string|null
     */
    protected function accountNumber($text)
    {
        $format = $this->format == self::FORMAT_CLASSIC ? '/^[0-9.]+/' : '/^[0-9A-Z]+/';
        if ($account = $this->getLine('25', $text)) {
            if (preg_match($format, $account, $match)) {
                return str_replace('.', '', $match[0]);
            }
        }

        return null;
    }

    /**
     * Rabobank does not use statement numbers. Use the opening balance
     * date as statement number instead.
     *
     * @param string $text Statement body text
     * @return string|null
     */
    protected function statementNumber($text)
    {
        if ($line = $this->getLine('60F', $text)) {
            if (preg_match('/(C|D)(\d{6})([A-Z]{3})([0-9,]{1,15})/', $line, $match)) {
                return $match[2];
            }
        }

        return null;
    }

    /**
     * Get the contra account from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountNumber(array $lines)
    {
        switch ($this->format) {
            case self::FORMAT_CLASSIC:
                if (preg_match('/(\d{6})((?:C|D)R?)([0-9,]{15})(N\d{3}|NMSC)([0-9P ]{16})/', $lines[0], $match)) {
                    return rtrim(ltrim($match[5], '0P'));
                }
                break;

            case self::FORMAT_STRUCTURED:
                $parts = explode("\r\n", $lines[0]);

                if (2 === count($parts)) {
                    return $parts[1];
                }
                break;
        }
    }

    /**
     * Get the contra account holder name from a transaction
     *
     * @param array $lines The transaction text at offset 0 and the description at offset 1
     * @return string|null
     */
    protected function contraAccountName(array $lines)
    {
        switch ($this->format) {
            case self::FORMAT_CLASSIC:
                if (preg_match('/(\d{6})((?:C|D)R?)([0-9,]{15})(N\d{3}|NMSC)([0-9P ]{16}|NONREF)(.*)/', $lines[0], $match)) {
                    return trim($match[6]) ?: null;
                }
                break;

            case self::FORMAT_STRUCTURED:
                if (preg_match('#/NAME/([^/]+)/#', $lines[1], $match)) {
                    return trim(str_replace("\r\n", '', $match[1])) ?: null;
                }
                break;
        }
    }
}