<?php

namespace Happyr\TranslationBundle\Service;

use Happyr\TranslationBundle\Exception\HappyrTranslationException;
use Happyr\TranslationBundle\Exception\HttpException;
use Happyr\TranslationBundle\Http\RequestManager;
use Happyr\TranslationBundle\Model\Message;
use Happyr\TranslationBundle\Translation\FilesystemUpdater;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class Loco implements TranslationServiceInterface
{
    const BASE_URL = 'https://localise.biz/api/';

    /**
     * @var RequestManager
     */
    private $requestManager;

    /**
     * @var array projects
     */
    private $projects;

    /**
     * @var FilesystemUpdater filesystemService
     */
    private $filesystemService;

    /**
     * @var TranslatorInterface filesystemService
     */
    private $translator;

    /**
     * @param TranslatorInterface $translator
     * @param RequestManager $requestManager
     * @param FilesystemUpdater $fs
     * @param array $projects
     */
    public function __construct(RequestManager $requestManager, FilesystemUpdater $fs, TranslatorInterface $translator, array $projects)
    {
        $this->translator = $translator;
        $this->requestManager = $requestManager;
        $this->projects = $projects;
        $this->filesystemService = $fs;
    }

    /**
     * @param $key
     * @param $method
     * @param $resource
     * @param null $body
     * @param string $type
     * @param array $extraQuery
     * @return array
     * @throws HttpException
     */
    protected function makeApiRequest($key, $method, $resource, $body = null, $type = 'form', $extraQuery = array())
    {
        $headers = array();
        if ($body !== null) {
            if ($type === 'form') {
                if (is_array($body)) {
                    $body = http_build_query($body);
                }
                $headers['Content-Type'] = 'application/x-www-form-urlencoded';
            } elseif ($type === 'json') {
                $body = json_encode($body);
                $headers['Content-Type'] = 'application/json';
            }
        }

        $query = array_merge($extraQuery, ['key' => $key]);
        $url = self::BASE_URL . $resource . '?' . http_build_query($query);

        return $this->requestManager->send($method, $url, $body, $headers);
    }

    /**
     * Fetch a translation form Loco.
     *
     * @param Message $message
     */
    public function fetchTranslation(Message $message, $updateFs = false)
    {
        $project = $this->getProject($message);

        try {
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
            $response = $this->makeApiRequest($project['api_key'], 'GET', $resource);
        } catch (HttpException $e) {
            if ($e->getCode() === 404) {
                //Message does not exist
                return;
            }
            throw $e;
        }

        $logoTranslation = $response['translation'];
        $messageTranslation = $message->getTranslation();
        $message->setTranslation($logoTranslation);

        // update filesystem
        if ($updateFs && $logoTranslation !== $messageTranslation) {
            $this->filesystemService->updateMessageCatalog([$message]);
        }

        return $logoTranslation;
    }

    /**
     * Update the translation in Loco.
     *
     * @param Message $message
     */
    public function updateTranslation(Message $message)
    {
        $project = $this->getProject($message);

        try {
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
            $this->makeApiRequest($project['api_key'], 'POST', $resource, $message->getTranslation());
        } catch (HttpException $e) {
            if ($e->getCode() === 404) {
                //Asset does not exist
                if ($this->createAsset($message)) {
                    //Try again
                    return $this->updateTranslation($message);
                }

                return false;
            }
            throw $e;
        }

        $this->filesystemService->updateMessageCatalog([$message]);

        return true;
    }

    /**
     * If there is something wrong with the translation, please flag it.
     *
     * @param Message $message
     * @param int $type 0: Fuzzy, 1: Incorrect, 2: Provisional, 3: Unapproved, 4: Incomplete
     *
     * @return bool
     */
    public function flagTranslation(Message $message, $type = 0)
    {
        $project = $this->getProject($message);
        $flags = ['fuzzy', 'incorrect', 'provisional', 'unapproved', 'incomplete'];

        try {
            $resource = sprintf('translations/%s/%s/flag', $message->getId(), $message->getLocale());
            $this->makeApiRequest($project['api_key'], 'POST', $resource, ['flag' => $flags[$type]]);
        } catch (HttpException $e) {
            if ($e->getCode() === 404) {
                //Message does not exist
                return false;
            }
            throw $e;
        }

        return true;
    }

    /**
     * Create a new asset in Loco.
     *
     * @param Message $message
     *
     * @return bool
     */
    public function createAsset(Message $message)
    {
        $project = $this->getProject($message);

        try {
            $response = $this->makeApiRequest($project['api_key'], 'POST', 'assets', [
                'id' => $message->getId(),
                'name' => $message->getId(),
                'type' => 'text',
                // Tell Loco not to translate the asset
                'default' => 'untranslated',
            ]);

            if ($message->hasParameters()) {
                // Send those parameter as a note to Loco
                $notes = '';
                foreach ($message->getParameters() as $key => $value) {
                    if (!is_array($value)) {
                        $notes .= 'Parameter: ' . $key . ' (i.e. : ' . $value . ")\n";
                    } else {
                        foreach ($value as $k => $v) {
                            $notes .= 'Parameter: ' . $k . ' (i.e. : ' . $v . ")\n";
                        }
                    }
                }

                $resource = sprintf('assets/%s.json', $message->getId());
                $this->makeApiRequest($project['api_key'], 'PATCH', $resource, ['notes' => $notes], 'json');
            }
        } catch (HttpException $e) {
            if ($e->getCode() === 409) {
                //conflict.. ignore
                return false;
            }
            throw $e;
        }

        // if this project has multiple domains. Make sure to tag it
        if (!empty($project['domains'])) {
            $this->addTagToAsset($project, $response['id'], $message->getDomain());
        }

        return true;
    }

    /**
     * @param Message $message
     *
     * @return array
     */
    protected function getProject(Message $message)
    {
        if (isset($this->projects[$message->getDomain()])) {
            return $this->projects[$message->getDomain()];
        }

        // Return the first project that has the correct domain and locale
        foreach ($this->projects as $project) {
            if (in_array($message->getDomain(), $project['domains'])) {
                if (in_array($message->getLocale(), $project['locales'])) {
                    return $project;
                }
            }
        }
    }

    /**
     * @param $project
     * @param $messageId
     * @param $domain
     */
    protected function addTagToAsset($project, $messageId, $domain)
    {
        $resource = sprintf('assets/%s/tags', $messageId);
        $this->makeApiRequest($project['api_key'], 'POST', $resource, ['name' => $domain]);
    }

    /**
     * Download all the translations from Loco. This will replace all the local files.
     * This is a quick method of getting all the latest translations and assets.
     */
    public function downloadAllTranslations()
    {
        $data = [];
        foreach ($this->projects as $name => $config) {
            if (empty($config['domains'])) {
                $this->getUrls($data, $config, $name, false);
            } else {
                foreach ($config['domains'] as $domain) {
                    $this->getUrls($data, $config, $domain, true);
                }
            }
        }
        $this->requestManager->downloadFiles($this->filesystemService, $data);
    }

    /**
     * Upload all the translations from the symfony project into Loco. This will override
     * every changed strings in loco
     */
    public function uploadAllTranslations()
    {
        foreach ($this->projects as $name => $config) {
            if (empty($config['domains'])) {
                $this->doUploadDomains($config, $name, false);
            } else {
                foreach ($config['domains'] as $domain) {
                    $this->doUploadDomains($config, $domain, true);
                }
            }
        }
    }

    /**
     * @param array $config
     * @param       $domain
     * @param       $useDomainAsFilter
     */
    protected function doUploadDomains(array &$config, $domain, $useDomainAsFilter)
    {
        $query = $this->getExportQueryParams($config['api_key']);

        if ($useDomainAsFilter) {
            $query['filter'] = $domain;
        }

        foreach ($config['locales'] as $locale) {
            $extension = $this->filesystemService->getFileExtension();
            $file = $this->filesystemService->getTargetDir();
            $file .= sprintf('/%s.%s.%s', $domain, $locale, $extension);

            if (is_file($file)) {
                $query = [
                    'index' => 'id',
                    'tag' => $domain,
                    'locale'=> $locale
                ];

                $resource = sprintf('import/%s', $extension);
                $response = $this->makeApiRequest($config['api_key'], 'POST', $resource, file_get_contents($file), 'form', $query);
                $this->flatten($response);
            } else {
                throw new FileNotFoundException(sprintf("Can't find %s file, perhaps you should generate the translations file ?", $file));
            }
        }
    }

    /**
     * Synchronize all the translations with Loco. This will keep placeholders. This function is slower
     * than just to download the translations.
     */
    public function synchronizeAllTranslations()
    {
        foreach ($this->projects as $name => $config) {
            if (empty($config['domains'])) {
                $this->doSynchronizeDomain($config, $name, false);
            } else {
                foreach ($config['domains'] as $domain) {
                    $this->doSynchronizeDomain($config, $domain, true);
                }
            }
        }
    }

    /**
     * @param array $config
     * @param       $domain
     * @param       $useDomainAsFilter
     */
    protected function doSynchronizeDomain(array &$config, $domain, $useDomainAsFilter)
    {
        $query = $this->getExportQueryParams($config['api_key']);

        if ($useDomainAsFilter) {
            $query['filter'] = $domain;
        }

        foreach ($config['locales'] as $locale) {
            $resource = sprintf('export/locale/%s.%s', $locale, 'json');
            $response = $this->makeApiRequest($config['api_key'], 'GET', $resource, ['query' => $query]);

            $this->flatten($response);

            $messages = array();
            foreach ($response as $id => $translation) {
                $messages[] = new Message([
                    'count' => 1,
                    'domain' => $domain,
                    'id' => $id,
                    'locale' => $locale,
                    'state' => 1,
                    'translation' => $translation,
                ]);
            }

            $this->filesystemService->updateMessageCatalog($messages);
        }
    }

    /**
     * Flattens an nested array of translations.
     *
     * The scheme used is:
     *   'key' => array('key2' => array('key3' => 'value'))
     * Becomes:
     *   'key.key2.key3' => 'value'
     *
     * This function takes an array by reference and will modify it
     *
     * @param array &$messages The array that will be flattened
     * @param array $subnode Current subnode being parsed, used internally for recursive calls
     * @param string $path Current path being parsed, used internally for recursive calls
     */
    private function flatten(array &$messages, array $subnode = null, $path = null)
    {
        if (null === $subnode) {
            $subnode = &$messages;
        }
        foreach ($subnode as $key => $value) {
            if (is_array($value)) {
                $nodePath = $path ? $path . '.' . $key : $key;
                $this->flatten($messages, $value, $nodePath);
                if (null === $path) {
                    unset($messages[$key]);
                }
            } elseif (null !== $path) {
                $messages[$path . '.' . $key] = $value;
            }
        }
    }

    /**
     * @param array $data
     * @param array $config
     * @param string $domain
     * @param bool $useDomainAsFilter
     */
    protected function getUrls(array &$data, array $config, $domain, $useDomainAsFilter)
    {
        $query = $this->getExportQueryParams($config['api_key']);

        if ($useDomainAsFilter) {
            $query['filter'] = $domain;
        }

        foreach ($config['locales'] as $locale) {
            // Build url
            $url = sprintf('%sexport/locale/%s.%s?%s', self::BASE_URL, $locale, $this->filesystemService->getFileExtension(), http_build_query($query));
            $fileName = sprintf('%s.%s.%s', $domain, $locale, $this->filesystemService->getFileExtension());

            $data[$url] = $fileName;
        }
    }

    /**
     * @param array $config
     *
     * @return array
     */
    private function getExportQueryParams($key)
    {
        $data = array(
            'index' => 'id',
            'status' => 'translated',
            'key' => $key,
        );
        switch ($this->filesystemService->getFileExtension()) {
            case 'php':
                $data['format'] = 'zend'; // 'Zend' will give us a flat array
                break;
            case 'xlf':
            default:
                $data['format'] = 'symfony';
        }

        return $data;
    }
}


<?php

namespace Happyr\TranslationBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * This command will import your translations into your translator service
 */
class UploadCommand extends ContainerAwareCommand
{
    const RETURN_CODE_NO_FORCE = 2;

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('happyr:translation:upload')
            ->addOption('force', null, InputOption::VALUE_NONE, 'Set this parameter to execute this action')
            ->setDescription('Upload your translations into your translator service.');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if ($input->getOption('force')) {
            $this->getContainer()->get('happyr.translation')->uploadAllTranslations();
            $output->writeln('Upload complete');
        } else {
            $output->writeln('<error>ATTENTION:</error> This operation should not be executed in a production environment.');
            $output->writeln('');
            $output->writeln(sprintf('<info>This is going to replace every translations on your translator service.</info>'));
            $output->writeln('Please run the operation with --force to execute');
            return self::RETURN_CODE_NO_FORCE;
        }
    }
}


<?php

namespace Happyr\TranslationBundle\Translation;

use Happyr\TranslationBundle\Model\Message;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Translation\Dumper\DumperInterface;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;

/**
 * @author Tobias Nyholm, Damien Harper
 *
 * Update the locale file system with changes in the catalogue
 */
class FilesystemUpdater
{
    /**
     * @var LoaderInterface loader
     */
    private $loader;

    /**
     * @var DumperInterface dumper
     */
    private $dumper;

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

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

    /**
     * @var Message[] messages
     */
    private $messages;

    /**
     * @var bool
     */
    private $syncEmptyTranslations;

    /**
     * Filesystem constructor.
     *
     * @param LoaderInterface $loader
     * @param DumperInterface $dumper
     * @param string          $targetDir
     * @param string          $fileExtension
     * @param bool            $syncEmptyTranslations
     */
    public function __construct(LoaderInterface $loader, DumperInterface $dumper, $targetDir, $fileExtension, $syncEmptyTranslations = true)
    {
        $this->loader = $loader;
        $this->dumper = $dumper;
        $this->targetDir = $targetDir;
        $this->messages = array();
        $this->fileExtension = $fileExtension;
        $this->syncEmptyTranslations = $syncEmptyTranslations;
    }

    /**
     * Returns translation file type.
     *
     * @return string
     */
    public function getFileExtension()
    {
        return $this->fileExtension;
    }

    /**
     * @param string $fileName
     * @param string $data     the file contents
     */
    public function writeToFile($fileName, $data)
    {
        if (!is_dir($this->targetDir)) {
            mkdir($this->targetDir, 0777, true);
        }

        file_put_contents(sprintf('%s/%s', $this->targetDir, $fileName), $data);
    }

    /**
     * Update message catalogues.
     *
     * @param Message[] $messages
     */
    public function updateMessageCatalog(array $messages)
    {
        $this->messages = array_merge($messages, $this->messages);
    }

    /**
     * Update the file system after the Response has been sent back to the client.
     *
     * @param Event $event
     *
     * @throws \ErrorException
     * @throws \Exception
     */
    public function onTerminate(Event $event)
    {
        if (empty($this->messages)) {
            return;
        }

        /** @var MessageCatalogue[] $catalogues */
        $catalogues = array();
        foreach ($this->messages as $m) {
            $key = $m->getLocale().$m->getDomain();
            if (!isset($catalogues[$key])) {
                $file = sprintf('%s/%s.%s.%s', $this->targetDir, $m->getDomain(), $m->getLocale(), $this->getFileExtension());

                try {
                    $catalogues[$key] = $this->loader->load($file, $m->getLocale(), $m->getDomain());
                } catch (NotFoundResourceException $e) {
                    $catalogues[$key] = new MessageCatalogue($m->getLocale());
                }
            }

            $translation = $m->getTranslation();
            if (empty($translation)) {
                if ($this->syncEmptyTranslations) {
                    $translation = sprintf('[%s]', $m->getId());
                } else {
                    continue;
                }
            }

            $catalogues[$key]->set($m->getId(), $translation, $m->getDomain());
        }

        foreach ($catalogues as $catalogue) {
            try {
                $this->dumper->dump($catalogue, ['path' => $this->targetDir]);
            } catch (\ErrorException $e) {
                // Could not save file
                // TODO better error handling
                throw $e;
            }
        }
    }

    /**
     * @return string
     */
    public function getTargetDir()
    {
        return $this->targetDir;
    }
}


<?php

namespace Happyr\TranslationBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class HappyrTranslationExtension extends Extension
{
    /**
     * @param array            $configs
     * @param ContainerBuilder $container
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);
        $this->copyValuesFromParentToProject('locales', $config);
        $this->copyValuesFromParentToProject('domains', $config);

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

        if ($config['auto_add_assets']) {
            $loader->load('autoAdd.yml');
        }

        $container->setParameter('translation.toolbar.allow_edit', $config['allow_edit']);

        $targetDir = rtrim($config['target_dir'], '/');
        $container->findDefinition('happyr.translation.filesystem')
            ->replaceArgument(2, $targetDir)
            ->replaceArgument(3, $config['file_extension'])
            ->replaceArgument(4, $config['sync_empty_translations']);

        $this->configureLoaderAndDumper($container, $config['file_extension']);

        $container->getDefinition('happyr.translation.request_manager')
            ->replaceArgument(0, new Reference($config['httplug_client']))
            ->replaceArgument(1, new Reference($config['httplug_message_factory']));

        /*
         * Set alias for the translation service
         */
        $container->setAlias('happyr.translation', 'happyr.translation.service.'.$config['translation_service']);

        $container->findDefinition('happyr.translation.service.loco')
            ->replaceArgument(3, $config['projects']);
    }

    /**
     * Copy the parent configuration to the children.
     *
     * @param string $key
     * @param array  $config
     */
    private function copyValuesFromParentToProject($key, array &$config)
    {
        if (empty($config[$key])) {
            return;
        }

        foreach ($config['projects'] as &$project) {
            if (empty($project[$key])) {
                $project[$key] = $config[$key];
            }
        }
    }

    /**
     * @param ContainerBuilder $container
     * @param string           $fileExtension
     */
    protected function configureLoaderAndDumper(ContainerBuilder $container, $fileExtension)
    {
        switch ($fileExtension) {
            case 'xlf':
                $fileExtension = 'xliff';
                break;
            case 'yml':
                $fileExtension = 'yaml';
                break;
        }

        $loader = $container->register('happyr.translation.loader', sprintf('Symfony\Component\Translation\Loader\%sFileLoader', ucfirst($fileExtension)));
        $loader->addTag('translation.loader', ['alias' => $fileExtension]);

        $dumper = $container->register('happyr.translation.dumper', sprintf('Symfony\Component\Translation\Dumper\%sFileDumper', ucfirst($fileExtension)));
        $dumper->addTag('translation.dumper', ['alias' => $fileExtension]);
    }
}


<?php

namespace Happyr\TranslationBundle\DependencyInjection;

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

/**
 * This is the class that validates and merges configuration from your app/config files.
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $root = $treeBuilder->root('happyr_translation');

        $root->children()
            ->enumNode('translation_service')->values(array('blackhole', 'filesystem', 'loco'))->defaultValue('loco')->end()
            ->scalarNode('target_dir')->defaultValue('%kernel.root_dir%/Resources/translations')->end()
            ->scalarNode('httplug_client')->defaultValue('httplug.client')->cannotBeEmpty()->end()
            ->scalarNode('httplug_message_factory')->defaultValue('httplug.message_factory')->cannotBeEmpty()->end()
            ->booleanNode('sync_empty_translations')->defaultTrue()->end()
            ->booleanNode('auto_add_assets')->defaultFalse()->end()
            ->booleanNode('allow_edit')->defaultTrue()->end()
            ->enumNode('file_extension')->values(array('csv', 'ini', 'json', 'mo', 'php', 'po', 'qt', 'yml', 'xlf'))->defaultValue('xlf')->end()
            ->arrayNode('locales')
                ->requiresAtLeastOneElement()
                ->prototype('scalar')->end()
            ->end()
            ->arrayNode('domains')
                ->requiresAtLeastOneElement()
                ->prototype('scalar')->end()
            ->end()
            ->append($this->getProjectNode())
        ->end();

        return $treeBuilder;
    }

    /**
     * @return \Symfony\Component\Config\Definition\Builder\NodeDefinition
     */
    private function getProjectNode()
    {
        $treeBuilder = new TreeBuilder();
        $node = $treeBuilder->root('projects');
        $node
            ->useAttributeAsKey('name')
            ->prototype('array')
            ->children()
                ->scalarNode('api_key')->isRequired()->end()
                ->arrayNode('locales')
                    ->requiresAtLeastOneElement()
                    ->prototype('scalar')->end()
                ->end()
                ->arrayNode('domains')
                    ->requiresAtLeastOneElement()
                    ->prototype('scalar')->end()
                ->end()
            ->end()
        ->end();

        return $node;
    }
}


<?php

namespace Happyr\TranslationBundle\Http;

use Happyr\TranslationBundle\Exception\HttpException;
use Happyr\TranslationBundle\Translation\FilesystemUpdater;
use Http\Client\Exception\TransferException;
use Http\Client\HttpClient;
use Http\Client\Plugin\ErrorPlugin;
use Http\Client\Plugin\LoggerPlugin;
use Http\Client\Plugin\PluginClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\MessageFactory;
use Psr\Log\LoggerInterface;

class RequestManager
{
    /**
     * @var HttpClient
     */
    private $client;

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

    /**
     *
     * @param HttpClient $client
     * @param MessageFactory $messageFactory
     */
    public function __construct(HttpClient $client, MessageFactory $messageFactory)
    {
        $this->client = new PluginClient($client, [new ErrorPlugin()]);
        $this->messageFactory = $messageFactory;
    }

    /**
     * @param array $data
     *
     * @return mixed
     */
    public function downloadFiles(FilesystemUpdater $filesystem, array $data)
    {
        $factory = $this->getMessageFactory();
        $client = $this->getClient();

        foreach ($data as $url => $fileName) {
            $response = $client->sendRequest($factory->createRequest('GET', $url));
            $filesystem->writeToFile($fileName, $response->getBody()->__toString());
        }
    }

    /**
     * @param string $method
     * @param string $url
     * @param array  $data
     *
     * @return array
     *
     * @throws HttpException
     */
    public function send($method, $url, $body = null, $headers = array())
    {
        $request = $this->getMessageFactory()->createRequest($method, $url, $headers, $body);

        try {
            $response = $this->getClient()->sendRequest($request);
        } catch (TransferException $e) {
            $message = 'Error sending request. ';
            if ($e instanceof \Http\Client\Exception\HttpException) {
                $message .= (string) $e->getResponse()->getBody();
            }

            throw new HttpException($message, $e->getCode(), $e);
        }

        // TODO add more error checks
        return json_decode($response->getBody()->__toString(), true);
    }

    /**
     * @return HttpClient
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     *
     * @return \Http\Message\MessageFactory
     */
    private function getMessageFactory()
    {
        return $this->messageFactory;
    }
}


<?php
/**
 * Copyright 2015, Martijn Croonen.
 * All rights reserved.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
namespace Phpcsp\Security;

/**
 * Class ContentSecurityPolicyBuilder
 *
 * A PHP helper class to dynamically construct Content Security Policy headers as defined by the W3C in the
 * Content Security Policy specification (http://www.w3.org/TR/CSP2/).
 */
class ContentSecurityPolicyHeaderBuilder
{
    /**
     * @var string
     */
    protected $headerName = 'Content-Security-Policy';

    /**
     * @var string
     */
    protected $reportOnlyHeaderName = 'Content-Security-Policy-Report-Only';

    /**
     * @var string
     */
    protected $legacyXssHeader = 'X-XSS-Protection';

    /**
     * @var string
     */
    protected $legacyFrameOptionsHeader = 'X-Frame-Options';

    /**
     * These are tokens defined by CSP and have specials meaning. They have to enclosed by quotes in the
     * CSP value string.
     *
     * @var array
     */
    protected $directiveValueTokens = ['self', 'unsafe-inline', 'unsafe-eval', 'unsafe-redirect', 'none'];

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-base-uri
     *
     * @var string
     */
    const DIRECTIVE_BASE_URI = 'base-uri';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-child-src
     *
     * @var string
     */
    const DIRECTIVE_CHILD_SRC = 'child-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-connect-src
     *
     * @var string
     */
    const DIRECTIVE_CONNECT_SRC = 'connect-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-default-src
     *
     * @var string
     */
    const DIRECTIVE_DEFAULT_SRC = 'default-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-font-src
     *
     * @var string
     */
    const DIRECTIVE_FONT_SRC = 'font-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-form-action
     *
     * @var string
     */
    const DIRECTIVE_FORM_ACTION = 'form-action';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-frame-ancestors
     *
     * @var string
     */
    const DIRECTIVE_FRAME_ANCESTORS = 'frame-ancestors';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-frame-src
     *
     * @var string
     */
    const DIRECTIVE_FRAME_SRC = 'frame-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-img-src
     *
     * @var string
     */
    const DIRECTIVE_IMG_SRC = 'img-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-media-src
     *
     * @var string
     */
    const DIRECTIVE_MEDIA_SRC = 'media-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-object-src
     *
     * @var string
     */
    const DIRECTIVE_OBJECT_SRC = 'object-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-script-src
     *
     * @var string
     */
    const DIRECTIVE_SCRIPT_SRC = 'script-src';

    /**
     * Valid CSP directive name. See http://www.w3.org/TR/CSP2/#directive-style-src
     *
     * @var string
     */
    const DIRECTIVE_STYLE_SRC = 'style-src';

    /**
     * A list of all valid CSP directives, without those (like reflected-xss) that require a defined set of values.
     *
     * @var array
     */
    protected $allowedDirectives = [
        self::DIRECTIVE_BASE_URI,
        self::DIRECTIVE_CHILD_SRC,
        self::DIRECTIVE_CONNECT_SRC,
        self::DIRECTIVE_DEFAULT_SRC,
        self::DIRECTIVE_FONT_SRC,
        self::DIRECTIVE_FORM_ACTION,
        self::DIRECTIVE_FRAME_ANCESTORS,
        self::DIRECTIVE_FRAME_SRC,
        self::DIRECTIVE_IMG_SRC,
        self::DIRECTIVE_MEDIA_SRC,
        self::DIRECTIVE_OBJECT_SRC,
        self::DIRECTIVE_SCRIPT_SRC,
        self::DIRECTIVE_STYLE_SRC
    ];

    /**
     * @var string
     */
    protected $directiveSeparator = '; ';

    /**
     * Contains all source sets that have been defined.
     *
     * @var array
     */
    protected $sourceSets = [];

    /**
     * Contains all the directives that have been defined and their respective values.
     *
     * @var array
     */
    protected $directives = [];

    /**
     * Valid value for the 'referrer' CSP directive. A user-agent should set an empty referrer value when
     * navigating away from a resource that sets the CSP 'referrer' value to 'none'.
     *
     * @var string
     */
    const REFERRER_NONE = 'none';

    /**
     * Valid value for the 'referrer' CSP directive. A user-agent should set an empty referrer value when
     * navigating to a resource that is net served over TLS from a resource that is served over TLS and sets the
     * CSP 'referrer' value to 'none-when-downgrading'.
     *
     * @var string
     */
    const REFERRER_NONE_WHEN_DOWNGRADE = 'none-when-downgrade';

    /**
     * Valid value for the 'referrer' CSP directive. A user-agent should set the resource's origin as the referrer value
     * when the resources sets the CSP 'referrer' value to 'origin'.
     *
     * @var string
     */
    const REFERRER_ORIGIN = 'origin';

    /**
     * Valid value for the 'referrer' CSP directive. A user-agent should set the resource's origin as the referrer value
     * when the resource sets the CSP 'referrer' value to 'origin-when-cross-origin' and it is navigating to a resource
     * from a different origin.
     *
     * @var string
     */
    const REFERRER_ORIGIN_WHEN_CROSS_ORIGIN = 'origin-when-cross-origin';

    /**
     * Valid value for the 'referrer' CSP directive. This it the default user-agent behaviour and could potentially
     * leak information.
     *
     * @var string
     */
    const REFERRER_UNSAFE_URL = 'unsafe-url';

    /**
     * All valid values for the 'referrer' CSP directive.
     *
     * @var array
     */
    protected $referrerValues = [
        self::REFERRER_NONE,
        self::REFERRER_NONE_WHEN_DOWNGRADE,
        self::REFERRER_ORIGIN,
        self::REFERRER_ORIGIN_WHEN_CROSS_ORIGIN,
        self::REFERRER_UNSAFE_URL,
        null
    ];

    /**
     * @var string|null
     */
    protected $referrerValue = null;

    /**
     * Valid valid value for the 'reflected-xss' CSP directive. A user agent must disable its active protections against
     * reflected cross-site scripting attacks for the protected resource. Try to avoid this value.
     *
     * @notice Same behaviour as 'X-XSS-Protection: 0'
     * @var string
     */
    const REFLECTED_XSS_ALLOW = 'allow';

    /**
     * Valid valid value for the 'reflected-xss' CSP directive. A user agent must stop rendering the protected resource
     * upon detection of reflected script, and instead act as if there was a fatal network error and no resource was
     * obtained, and report a violation.
     *
     * @notice Same behaviour as ' X-XSS-Protection: 1; mode=block'
     * @var string
     */
    const REFLECTED_XSS_BLOCK = 'block';

    /**
     * Valid valid value for the 'reflected-xss' CSP directive. A user agent must enable its active protections against
     * reflected cross-site scripting attacks for the protected resource. This might result in filtering script that is
     * believed to be reflected being filtered or selectively blocking script execution.
     *
     * @notice Same behaviour as 'X-XSS-Protection: 1'
     * @var string
     */
    const REFLECTED_XSS_FILTER = 'filter';

    /**
     * All valid values for the 'reflected-xss' CSP directive. This directive related to the deprecated
     * 'X-XSS-Protection' header.
     *
     * @var array
     */
    protected $reflectedXssValues = [
        self::REFLECTED_XSS_ALLOW,
        self::REFLECTED_XSS_BLOCK,
        self::REFLECTED_XSS_FILTER,
        null
    ];

    /**
     * Valid value for the 'X-Frame-Options' header. UA's will refuse to load any resource that sets the value of this
     * header to 'DENY' as part of Frame, iFrame, Object, Applet, or embed tag.
     *
     * @var string
     */
    const FRAME_OPTION_DENY = 'DENY';

    /**
     * Valid value for the 'X-Frame-Options' header. UA's will refuse to load any resource that sets the value of this
     * header to 'SAMEORIGIN' as part of Frame, iFrame, Object, Applet, or embed tag when the requesting resource is
     * from a different origin.
     *
     * @var string
     */
    const FRAME_OPTION_SAME_ORIGIN = 'SAMEORIGIN';

    /**
     * Valid value for the 'X-Frame-Options' header. UA's will refuse to load any resource that sets the value of this
     * header to 'ALLOW-FROM [origin]' as part of Frame, iFrame, Object, Applet, or embed tag when the requesting
     * resource is not within the specified origin.
     *
     * @var string
     */
    const FRAME_OPTION_ALLOW_FROM = 'ALLOW-FROM %s';

    /**
     * All valid values for the 'X-Frame-Options' header.
     *
     * @var array
     */
    protected $FrameOptionsValues = [
        self::FRAME_OPTION_DENY,
        self::FRAME_OPTION_SAME_ORIGIN,
        self::FRAME_OPTION_ALLOW_FROM,
        null
    ];

    /**
     * SHA 256 hash indicator.
     *
     * @var string
     */
    const HASH_SHA_256 = 'sha256';

    /**
     * SHA 384 hash indicator.
     *
     * @var string
     */
    const HASH_SHA_384 = 'sha384';

    /**
     * SHA 512 hash indicator.
     *
     * @var string
     */
    const HASH_SHA_512 = 'sha512';

    /**
     * Supported hash algorithms.
     *
     * @var array
     */
    protected $hashAlgorithmValues = [
        self::HASH_SHA_256,
        self::HASH_SHA_384,
        self::HASH_SHA_512
    ];

    /**
     * Holds the value for the 'X-Frame-Options' header when set.
     *
     * @var string|null
     */
    protected $frameOptionsValue = null;

    /**
     * The 'reflected-xss' CSP directive value.
     *
     * @notice Defines the same behaviour as the 'X-XSS-Protection' header
     * @var null
     */
    protected $reflectedXssValue = null;

    /**
     * When false, a report-only CSP header will be served. This means the policy will not be enforced by the UA but
     * violations will be reported.
     *
     * @var bool
     */
    protected $enforcePolicy = true;

    /**
     * When set to a valid URI, the UA will send violation reports to this URI.
     *
     * @var null
     */
    protected $reportUri = null;

    /**
     * @var string
     */
    protected $upgradeInsecureRequestsDirective = 'upgrade-insecure-requests';

    /**
     * @var bool
     */
    protected $upgradeInsecureRequests = false;

    /**
     * @param bool $enforce
     */
    public function enforcePolicy($enforce)
    {
        $this->enforcePolicy = $enforce;
    }

    /**
     * @return bool
     */
    public function shouldEnforcePolicy()
    {
        return $this->enforcePolicy;
    }

    /**
     * @param string $policy
     * @throws InvalidValueException
     */
    public function setReferrerPolicy($policy)
    {
        if (!in_array($policy, $this->referrerValues)) {
            throw new InvalidValueException(sprintf(
                'Tried to set the CSP referrer policy to "%s" which is an invalid value.',
                $policy
            ));
        }

        $this->referrerValue = $policy;
    }

    /**
     * @param string $policy
     * @throws InvalidValueException
     */
    public function setReflectedXssPolicy($policy)
    {
        if (!in_array($policy, $this->reflectedXssValues)) {
            throw new InvalidValueException(sprintf(
                'Tried to set the CSP reflected XSS policy to "%s" which is an invalid value.',
                $policy
            ));
        }

        $this->reflectedXssValue = $policy;
    }

    /**
     * @param string $policy
     * @param string $origin
     * @throws InvalidValueException
     */
    public function setFrameOptions($policy, $origin = '')
    {
        if (!in_array($policy, $this->FrameOptionsValues)) {
            throw new InvalidValueException(sprintf(
                'Tried to set the X-Frame-Options header to "%s" which is an invalid value.',
                $policy
            ));
        }

        if ($policy == static::FRAME_OPTION_ALLOW_FROM) {
            $origin = static::extractOrigin($origin);

            if (!$origin) {
                throw new InvalidOriginException();
            }
        }

        $this->frameOptionsValue = trim(sprintf($policy, $origin));
    }

    /**
     * @param bool $upgrade
     */
    public function setUpgradeInsecureRequests($upgrade)
    {
        $this->upgradeInsecureRequests = $upgrade;
    }

    /**
     * @param string $uri
     */
    public function setReportUri($uri)
    {
        $this->reportUri = $uri;
    }

    /**
     * @param string $name
     * @param array $expressions
     */
    public function defineSourceSet($name, array $expressions)
    {
        $this->sourceSets[$name] = $expressions;
    }

    /**
     * @param string $directive
     * @param string $nonce
     * @throws InvalidDirectiveException
     */
    public function addNonce($directive, $nonce)
    {
        if (!in_array($directive, $this->allowedDirectives)) {
            throw new InvalidDirectiveException('Tried to add a CSP nonce for an invalid directive.');
        }

        if (!(isset($this->directives[$directive]) && is_array($this->directives[$directive]))) {
            $this->directives[$directive] = [];
        }

        if (!(isset($this->directives[$directive]['nonces']) && is_array($this->directives[$directive]['nonces']))) {
            $this->directives[$directive]['nonces'] = [];
        }

        $this->directives[$directive]['nonces'][] = $nonce;
    }

    /**
     * @param string $directive
     * @param string $set
     * @throws InvalidDirectiveException
     * @throws SourceSetNotFoundException
     */
    public function addSourceSet($directive, $set)
    {
        if (!in_array($directive, $this->allowedDirectives)) {
            throw new InvalidDirectiveException(
                'Tried to add a source set for an CSP invalid directive.'
            );
        }

        if (!isset($this->sourceSets[$set])) {
            throw new SourceSetNotFoundException(sprintf(
                'Tried to add "%s" as a source set for the "%s" CSP directive but the source set has not been defined',
                $set,
                $directive
            ));
        }

        if (!(isset($this->directives[$directive]) && is_array($this->directives[$directive]))) {
            $this->directives[$directive] = [];
        }

        if (!(isset($this->directives[$directive]['sets']) && is_array($this->directives[$directive]['sets']))) {
            $this->directives[$directive]['sets'] = [];
        }

        $this->directives[$directive]['sets'][] = $set;
    }

    /**
     * @param string $directive
     * @param string $expression
     * @throws InvalidDirectiveException
     */
    public function addSourceExpression($directive, $expression)
    {
        if (!in_array($directive, $this->allowedDirectives)) {
            throw new InvalidDirectiveException(
                'Tried to add a source set for an CSP invalid directive.'
            );
        }

        if (!(isset($this->directives[$directive]) && is_array($this->directives[$directive]))) {
            $this->directives[$directive] = [];
        }

        if (!(
            isset($this->directives[$directive]['expressions'])
            && is_array($this->directives[$directive]['expressions'])
        )
        ) {
            $this->directives[$directive]['expressions'] = [];
        }

        $this->directives[$directive]['expressions'][] = $expression;
    }

    /**
     * Add a hash value to the script-src.
     *
     * @param string $type
     * @param string $hash
     * @throws InvalidDirectiveException
     */
    public function addHash($type, $hash)
    {
        $directive = self::DIRECTIVE_SCRIPT_SRC;
        if (!(isset($this->directives[$directive]) && is_array($this->directives[$directive]))) {
            $this->directives[$directive] = [];
        }

        if (!(isset($this->directives[$directive]['hashes']) && is_array($this->directives[$directive]['hashes']))) {
            $this->directives[$directive]['hashes'] = [];
        }

        $this->directives[$directive]['hashes'][$type][] = $hash;
    }

    /**
     * Returns the CSP header that should be used (enforced mode or report-only mode).
     *
     * @return string
     */
    public function getHeaderName()
    {
        if ($this->shouldEnforcePolicy()) {
            return $this->headerName;
        } else {
            return $this->reportOnlyHeaderName;
        }
    }

    /**
     * Returns the value for the CSP header based on the loaded configuration.
     *
     * @return string|null
     */
    public function getValue()
    {
        $directives = [];
        foreach ($this->directives as $name => $value) {
            $directives[] = sprintf('%s %s', $name, $this->parseDirectiveValue($value));
        }

        if (!is_null($this->reflectedXssValue)) {
            $directives[] = sprintf('%s %s', 'reflected-xss', $this->reflectedXssValue);
        }

        if ($this->upgradeInsecureRequests) {
            $directives[] = $this->upgradeInsecureRequestsDirective;
        }

        if (!is_null($this->referrerValue)) {
            $directives[] = sprintf('%s %s', 'referrer', $this->referrerValue);
        }

        if (!is_null($this->reportUri)) {
            $directives[] = sprintf('%s %s', 'report-uri', $this->reportUri);
        }

        // No CSP policies set?
        if (count($directives) < 1) {
            return null;
        }

        return trim(sprintf('%s%s', implode($this->directiveSeparator, $directives), $this->directiveSeparator));
    }

    /**
     * @param string $includeLegacy
     * @return array
     */
    public function getHeaders($includeLegacy)
    {
        $value = $this->getValue();
        if (is_null($value)) {
            $headers = [];
        } else {
            $headers[] = [
                'name'  => $this->getHeaderName(),
                'value' => $value
            ];
        }

        if ($includeLegacy) {
            return array_merge($headers, $this->getLegacyHeaders());
        }

        return $headers;
    }

    /**
     * @return array
     */
    public function getLegacyHeaders()
    {
        return array_filter([
            $this->getLegacyXssHeader($this->reflectedXssValue),
            $this->getLegacyFrameOptionsHeader()
        ]);
    }

    /**
     * @param array $directive
     * @return null|string
     */
    private function parseDirectiveValue($directive)
    {
        $expressions = [];

        if (!(isset($directive) && is_array($directive))) {
            return null;
        }

        // Parse the source expressions
        if (isset($directive['expressions']) && is_array($directive['expressions'])) {
            $expressions = $directive['expressions'];
        }

        // Parse the source sets
        if (isset($directive['sets']) && is_array($directive['sets'])) {
            foreach ($directive['sets'] as $set) {
                $expressions = array_merge($expressions, $this->sourceSets[$set]);
            }
        }

        // Parse the nonces
        if (isset($directive['nonces']) && is_array($directive['nonces'])) {
            foreach ($directive['nonces'] as $nonce) {
                $expressions[] = sprintf("'nonce-%s'", $nonce);
            }
        }

        // Parse the hashes
        if (isset($directive['hashes']) && is_array($directive['hashes'])) {
            foreach ($directive['hashes'] as $type => $hashes) {
                foreach ($hashes as $hash) {
                    $expressions[] = sprintf("'%s-%s'", $type, base64_encode($hash));
                }
            }
        }

        return trim(implode(' ', array_map(function ($value) {
            return $this->encodeDirectiveValue($value);
        }, $expressions)));
    }

    /**
     * @param string $value
     * @return string
     */
    public function encodeDirectiveValue($value)
    {
        $value = str_replace([';', ','], ['%3B', '%2C'], $value);

        if (in_array($value, $this->directiveValueTokens)) {
            $value = sprintf("'%s'", $value);
        }

        return trim($value);
    }

    /**
     * Extracts the (serialized) origin from a uri or false on failure.
     *
     * @param string $uri
     * @return false|string
     */
    public static function extractOrigin($uri)
    {
        $parts = parse_url($uri);

        // parse_url returns false when the uri is seriously malformed
        if (!is_array($parts)) {
            return false;
        }

        if (isset($parts['scheme']) && isset($parts['host'])) {
            if (isset($parts['port'])) {
                return sprintf('%s://%s:%s', $parts['scheme'], $parts['host'], $parts['port']);
            } else {
                return sprintf('%s://%s', $parts['scheme'], $parts['host']);
            }
        }

        return false;
    }

    /**
     * @param string $reflectedXssValue
     * @return array
     */
    private function getLegacyXssHeader($reflectedXssValue)
    {
        $header = [];

        switch ($reflectedXssValue) {
            case 'allow':
                $header = [
                    'name'  => $this->legacyXssHeader,
                    'value' => '0'
                ];
                break;

            case 'filter':
                $header = [
                    'name'  => $this->legacyXssHeader,
                    'value' => '1'
                ];
                break;

            case 'block':
                $header = [
                    'name'  => $this->legacyXssHeader,
                    'value' => '1; mode=block'
                ];
                break;
        }

        return $header;
    }

    /**
     * @return array
     */
    private function getLegacyFrameOptionsHeader()
    {
        if (is_null($this->frameOptionsValue)) {
            return [];
        }

        return [
            'name'  => $this->legacyFrameOptionsHeader,
            'value' => $this->frameOptionsValue
        ];
    }
}


<?php declare(strict_types=1);
////////////////////////////////////////////////////////////////////////////////
// __________ __             ________                   __________
// \______   \  |__ ______  /  _____/  ____ _____ ______\______   \ _______  ___
//  |     ___/  |  \\____ \/   \  ____/ __ \\__  \\_  __ \    |  _//  _ \  \/  /
//  |    |   |   Y  \  |_> >    \_\  \  ___/ / __ \|  | \/    |   (  <_> >    <
//  |____|   |___|  /   __/ \______  /\___  >____  /__|  |______  /\____/__/\_ \
//                \/|__|           \/     \/     \/             \/            \/
// -----------------------------------------------------------------------------
//          Designed and Developed by Brad Jones <brad @="bjc.id.au" />
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////

namespace Gears;

use Closure;
use Exception;
use Traversable;
use ArrayIterator;
use ReflectionClass;
use ReflectionException;
use Gears\String\Str;
use Composer\Autoload\ClassLoader;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

class ClassFinder implements IClassFinder
{
    /**
     * The composer class loader as returned from `vendor/autoload.php`.
     *
     * @var ClassLoader
     */
    protected $composer;

    /**
     * This will be filled with fully qualified class names as they are found
     * by searching through the various class maps provided by composer.
     *
     * @var array|string[]
     */
    protected $foundClasses = [];

    /**
     * The namespace to filter by will be stored here.
     *
     * > NOTE: This must be set, otherwise we could have a
     *         very large data set to search through.
     *
     * @var string
     */
    protected $namespace;

    /**
     * The interface name to filter by will be stored here.
     *
     * @var string
     */
    protected $implements;

    /**
     * The parent class to filter by will be stored here.
     *
     * @var string
     */
    protected $extends;

    /**
     * An optional custom filter method can be set.
     * Otherwise we will use the `defaultFilter` method in this class.
     *
     * @var Closure
     */
    protected $filter;

    /**
     * Constructor.
     *
     * @param ClassLoader $composer We rely on the information provided by the
     *                              composer class maps in order to find classes
     *                              for you.
     */
    public function __construct(ClassLoader $composer)
    {
        $this->composer = $composer;
    }

    public function namespace(string $namespace): IClassFinder
    {
        $this->namespace = $namespace; return $this;
    }

    public function implements(string $interface): IClassFinder
    {
        $this->implements = $interface; return $this;
    }

    public function extends(string $parent): IClassFinder
    {
        $this->extends = $parent; return $this;
    }

    public function filterBy(Closure $filter): IClassFinder
    {
        if ($this->implements !== null || $this->extends != null)
        {
            throw new Exception
            (
                'Can not set a custom filter and filter '.
                'by `implements` or `extends`!'
            );
        }

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

    public function search(): array
    {
        $this->foundClasses = [];

        if ($this->namespace === null)
        {
            throw new Exception('Namespace must be set!');
        }

        $this->searchClassMap();
        $this->searchPsrMaps();
        $this->runFilter();

        $this->namespace = null;
        $this->implements = null;
        $this->extends = null;

        return $this->foundClasses;
    }

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->search());
    }

    public function count(): int
    {
        return iterator_count($this->getIterator());
    }

    /**
     * Searches the composer class map.
     *
     * Results are added to the `$foundClasses` array.
     *
     * @return void
     */
    protected function searchClassMap()
    {
        foreach ($this->composer->getClassMap() as $fqcn => $file)
        {
            if (Str::s($fqcn)->is($this->namespace.'*'))
            {
                $this->foundClasses[realpath($file)] = $fqcn;
            }
        }
    }

    /**
     * Searches the composer PSR-x class maps.
     *
     * Results are added to the `$foundClasses` array.
     *
     * @return void
     */
    protected function searchPsrMaps()
    {
        $prefixes = array_merge
        (
            $this->composer->getPrefixes(),
            $this->composer->getPrefixesPsr4()
        );

        $trimmedNs = Str::s($this->namespace)->trimRight('\\');

        $nsSegments = $trimmedNs->split('\\');

        foreach ($prefixes as $ns => $dirs)
        {
            $foundSegments = Str::s($ns)->trimRight('\\')
            ->longestCommonPrefix($trimmedNs)->split('\\');

            foreach ($foundSegments as $key => $segment)
            {
                if ((string) $nsSegments[$key] !== (string) $segment)
                {
                    continue 2;
                }
            }

            foreach ($dirs as $dir)
            {
                foreach ((new Finder)->in($dir)->files()->name('*.php') as $file)
                {
                    if ($file instanceof SplFileInfo)
                    {
                        $fqcn = (string)Str::s($file->getRelativePathname())
                        ->trimRight('.php')
                        ->replace('/', '\\')
                        ->ensureLeft($ns);

                        if (Str::s($fqcn)->is($this->namespace.'*'))
                        {
                            $this->foundClasses[$file->getRealPath()] = $fqcn;
                        }
                    }
                }
            }
        }
    }

    /**
     * Runs a filter over the array of `$foundCLasses`.
     *
     * Also ensures the classes are real by creating a ReflectionClass instance.
     * By default we use the `defaultFilter` method.
     *
     * @return void
     */
    protected function runFilter()
    {
        foreach ($this->foundClasses as $file => $fqcn)
        {
            try
            {
                $rClass = new ReflectionClass($fqcn);
            }
            catch (ReflectionException $e)
            {
                $result = false;
            }

            if ($this->filter === null)
            {
                $result = $this->defaultFilter($rClass);
            }
            else
            {
                $result = call_user_func($this->filter, $rClass);
            }

            if ($result === false)
            {
                unset($this->foundClasses[$file]);
            }
        }
    }

    /**
     * The default filter run by `runFilter()`.
     *
     * Further filters by  interface or parent class and also filters
     * out actual Interfaces, Abstract Classes and Traits.
     *
     * @param  ReflectionClass $rClass
     * @return bool
     */
    protected function defaultFilter(ReflectionClass $rClass)
    {
        if ($this->implements !== null)
        {
            return $rClass->implementsInterface($this->implements) && !$rClass->isInterface();
        }

        if ($this->extends !== null)
        {
            return $rClass->isSubclassOf($this->extends) && !$rClass->isAbstract();
        }

        return (!$rClass->isInterface() && !$rClass->isAbstract() && !$rClass->isTrait());
    }
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository;

use Illuminate\Database\Eloquent\Model;
use anlutro\LaravelValidation\ValidatorInterface;

/**
 * Abstract Eloquent repository that provides some basic functionality.
 */
abstract class EloquentRepository extends AbstractRepository
{
	/**
	 * @var \Illuminate\Database\Eloquent\Model
	 */
	protected $model;

	/**
	 * Whether to call push() or just save() when creating/updating a model.
	 *
	 * @var boolean
	 */
	protected $push = false;

	/**
	 * @param \Illuminate\Database\Eloquent\Model $model
	 * @param \anlutro\LaravelValidation\ValidatorInterface $validator
	 */
	public function __construct(Model $model, ValidatorInterface $validator = null)
	{
		parent::__construct($validator);

		$this->model = $model;

		if ($validator) {
			$validator->replace('table', $this->model->getTable());
		}
	}

	/**
	 * Set the repository's model.
	 *
	 * @param  $model  \Illuminate\Database\Eloquent\Model
	 *
	 * @return $this
	 */
	public function setModel($model)
	{
		$this->model = $model;

		return $this;
	}

	/**
	 * Get the repository's model.
	 *
	 * @return \Illuminate\Database\Eloquent\Model
	 */
	public function getModel()
	{
		return $this->model;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getConnection()
	{
		return $this->model->getConnection();
	}

	/**
	 * {@inheritdoc}
	 *
	 * @return  \Illuminate\Database\Eloquent\Model
	 */
	public function getNew(array $attributes = array())
	{
		return $this->model->newInstance($attributes);
	}

	/**
	 * {@inheritdoc}
	 */
	public function update($model, array $attributes)
	{
		if (!$model->exists) {
			throw new \RuntimeException('Cannot update non-existant model');
		}

		return parent::update($model, $attributes);
	}

	/**
	 * {@inheritdoc}
	 *
	 * @return  \Illuminate\Database\Eloquent\Model
	 */
	protected function performCreate($model, array $attributes)
	{
		$model->fill($attributes);

		return $this->perform('save', $model, $attributes, false);
	}

	/**
	 * {@inheritdoc}
	 */
	protected function performUpdate($model, array $attributes)
	{
		$model->fill($attributes);

		return $this->perform('save', $model, $attributes, false);
	}

	/**
	 * {@inheritdoc}
	 */
	protected function performSave($model, array $attributes)
	{
		$method = $this->push ? 'push' : 'save';

		return $model->$method() ? $model : false;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function performDelete($model)
	{
		return $model->delete();
	}

	/**
	 * {@inheritdoc}
	 *
	 * @return \Illuminate\Database\Eloquent\Builder
	 */
	protected function newQuery()
	{
		return $this->model->newQuery();
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getKeyName()
	{
		return $this->model->getQualifiedKeyName();
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getEntityKey($model)
	{
		return $model->getKey();
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getEntityAttributes($model)
	{
		return $model->getAttributes();
	}
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository;

class SoftDeletingEloquentRepository extends EloquentRepository
{

    /**
     * @var boolean
     */
    protected $onlyTrashed = false;

    /**
     * @var boolean
     */
    protected $withTrashed = false;

    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     */
    public function restore($model)
    {
        $model->restore();
    }

    /**
     * Limit to trashed entities.
     */
    public function onlyTrashed()
    {
        $this->onlyTrashed = true;
        $this->withTrashed = false;
    }

    /**
     * Return trashed entites aswell.
     */
    public function withTrashed()
    {
        $this->onlyTrashed = false;
        $this->withTrashed = true;
    }

    /**
     * {@inheritdoc}
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    protected function newQuery()
    {
        $query = parent::newQuery();

        if ($this->onlyTrashed === true) {
            $query->onlyTrashed();
        }

        if ($this->withTrashed === true) {
            $query->withTrashed();
        }

        return $query;
    }

}


<?php
/**
 * An example repository for projects.
 *
 * $projectRepo->onlyActive()->getAll();
 * $projectRepo->onlyInactive()->getAll();
 * $projectRepo->onlyInactive()->paginate(20)->getAll();
 * $projectRepo->toggleExceptions(true)->getByKey($id);
 */
class ProjectRepository extends \anlutro\LaravelRepository\EloquentRepository
{
    /**
     * Override the constructor. In PHP, you can override type hints in constructors,
     * but not in normal methods. Weird behaviour but we can use this to our
     * advantage.
     */
    public function __construct(ProjectModel $project, ProjectValidator $validator)
    {
        parent::__construct($project, $validator);
    }

    /**
     * Whether to get all projects regardless of status (null), only active
     * projects (true) or only inactive projects (false).
     */
    protected $active = null;

    /**
     * Get only active projects.
     *
     * @return static
     */
    public function onlyActive()
    {
        $this->active = true;
        return $this;
    }

    /**
     * Get only inactive projects.
     *
     * @return static
     */
    public function onlyInactive()
    {
        $this->active = false;
        return $this;
    }

    /**
     * You can add as many setter/getter methods to your repositories as you
     * wish if you need extra behaviour. For this example, let's assume the
     * repository needs access to the currently logged in user. Instead of using
     * Auth::user() in the repository, we add a setter method which we can call
     * from the controller - $repo->setCurrentUser(Auth::user());
     */
    public function setCurrentUser(MyUserModel $user)
    {
        $this->user = $user;
    }

    /**
     * Determines if a model can be stored to the database or not. Use it for
     * stuff that can't (easily) be done by the validator.
     *
     * Changed from readyForCreate in 0.5
     */
    protected function beforeCreate($project, $attributes)
    {
        // each project has an owner (a user). let's make sure that the project
        // owner is an active user - if not, add an error and return false. we
        // assume that project->owner is always populated.
        if (!$project->owner->is_active) {
            $this->errors->add('owner', 'Project owner must be an active user.');
            return false;
        }

        return true;
    }

    /**
     * Using afterSave, afterCreate or afterUpdate to set relationships is
     * often very useful. afterSave is called on both update and create.
     */
    public function afterSave($project, $attributes)
    {
        // if "relation" is not present in the input, we simply detach all
        // related models by passing an empty array.
        $ids = isset($attributes['relation']) ? $attributes['relation'] : [];
        $project->manyToManyRelation()->sync($ids);
    }

    public function afterCreate($project, $attributes)
    {
        if (!$project->user && $this->user) {
            $project->owner()->associate($this->user);
        }
    }

    /**
     * This method is called before fetchMany and fetchSingle. We use it to add
     * functionality that should be present on every query.
     *
     * Renamed from prepareQuery in 0.5
     */
    protected function beforeQuery($query, $many)
    {
        // if this->active has been set to something, we add it to the query
        if ($this->active !== null) {
            $query->where('is_active', '=', $this->active);
        }

        // we want to always eager load members and comments.
        $query->with('members', 'comments');

        // let's say guests can only view projects with access level 1 or less.
        if (!$this->user) {
            $query->where('access_level', '<=', 1);
        }

        // unless user is an admin, we restrict every query to only show
        // projects with access_level lower than the logged in user's.
        else if (!$this->user->isAdmin()) {
            $query->where('access_level', '<=', $user->access_level);
        }
    }

    /**
     * This method is called after fetchMany and fetchSingle. It is most useful
     * to append attributes onto more than one model for stuff that can't easily
     * be done via the query builder. Sometimes, for example, a subselect or
     * join or groupby can mess with the paginator.
     *
     * Changed from prepareCollection/preparePaginator in 0.5
     */
    protected function afterQuery($result)
    {
        // $result can be a single model, a paginator or a collection. We can
        // standardize the variable in this way to be able to use the same code
        // on all three types.
        if ($result instanceof \Illuminate\Database\Eloquent\Model) {
            // we might just do `return;` here if we didn't want our logic
            // applied to a single model.
            $projects = $result->newCollection([$result]);
        } else if ($result instanceof \Illuminate\Pagination\Paginator) {
            $projects = $result->getCollection();
        }

        // let's say we want to add some data onto the models after they've
        // been retrieved from the database. we want to add the count of a
        // relation, but don't want to mess with sub-selects and don't want to
        // waste memory by eager loading the relation, nor do we want to run
        // one query for each model.

        // build the query that gets the relationship count. some of this logic
        // could be moved into model scopes if you want. this is a rather
        // complex query so if you don't understand what is going on here,
        // don't worry too much. basically just getting the number of relations
        // attached to each project.
        $results = $this->model->relation()
            ->getRelated()->newQuery()
            ->whereIn('parent_id', $projects->lists('id'))
            ->selectRaw('parent_id, count(*) as relation_count')
            ->groupBy('parent_id')
            ->lists('relation_count', 'parent_id');

        // now add the attribute to each model.
        $projects->each(function($project) {
            $project->relation_count = $results[$project->id];
        });
    }
}


<?php
/**
 * This is a real repository from one of my real world projects. I will not
 * bother explaining too much what is going on in it, but it might serve as an
 * example of the type of things you can do.
 */

namespace REDACTED\Wastage;

use anlutro\LaravelRepository\EloquentRepository;
use Illuminate\Database\Eloquent\Collection;

use REDACTED\Departments\DepartmentModel;
use REDACTED\Products\ProductModel;

class ItemRepository extends EloquentRepository
{
    protected $wastage;
    protected $department;
    protected $product;

    public function __construct(ItemModel $model, ItemValidator $validator)
    {
        parent::__construct($model, $validator);
    }

    public function setWastage(WastageModel $wastage)
    {
        $this->wastage = $wastage;
        $this->setDepartment($wastage->department);
    }

    public function setDepartment(DepartmentModel $department)
    {
        $this->department = $department;
    }

    public function setProduct(ProductModel $product)
    {
        $this->product = $product;
    }

    /**
     * Sync item data for a new wastage.
     *
     * @param  \REDACTED\Wastage\WastageModel            $wastage
     * @param  \Illuminate\Database\Eloquent\Collection  $products
     * @param  array                                     $items    Item input
     *
     * @return array  Array of WastageItem models
     */
    public function syncNewWastageItems(WastageModel $wastage, Collection $products, array $items)
    {
        $this->setWastage($wastage);

        $itemModels = [];

        foreach ($items as $key => $value) {
            $data = $this->buildItemData($key, $value, $products);
            $item = $this->create($data);

            $itemModels[] = $item;
        }

        return $itemModels;
    }

    /**
     * Sync items for an existing wastage. Updates the item entry if it exists,
     * or creates them if they don't.
     *
     * @param  \REDACTED\Wastage\WastageModel            $wastage
     * @param  \Illuminate\Database\Eloquent\Collection  $products
     * @param  array                                     $items    Item input
     *
     * @return array  Array of WastageItem models
     */
    public function syncExistingWastageItems(WastageModel $wastage, Collection $products, array $items)
    {
        $itemModels = [];

        foreach ($items as $key => $value) {
            if ($item = $this->findProductItem($wastage->items, $key)) {
                $this->checkIntegrity($item);
                $item->update($value);
            } else {
                $data = $this->buildItemData($key, $value, $products);
                $item = $this->create($data);
            }

            $itemModels[] = $item;
        }

        return $itemModels;
    }

    /**
     * Find the item entry of a specific product key in a collection of items.
     *
     * @param  \Illuminate\Database\Eloquent\Collection $items
     * @param  mixed                                    $key   The product's primary key.
     *
     * @return \REDACTED\Wastage\ItemModel|null
     */
    protected function findProductItem(Collection $items, $key)
    {
        if ($key instanceof ItemModel) {
            $key = $key->getKey();
        }

        return $items->first(function($id, $item) use($key) {
            return $item->product_id == $key;
        });
    }

    /**
     * Check the integrity of an item model. Add missing data if necessary.
     *
     * @param  \REDACTED\Wastage\ItemModel $item
     *
     * @return void
     */
    protected function checkIntegrity(ItemModel $item)
    {
        if ($item->department_id < 1) {
            $item->department()->associate($this->department);
        }

        if ($item->wastage_id < 1) {
            $item->wastage()->associate($this->wastage);
        }
    }

    /**
     * Build the pivot table data for a particular item.
     *
     * @param  mixed                                    $key      The product's primary key.
     * @param  array                                    $data
     * @param  \Illuminate\Database\Eloquent\Collection $products
     *
     * @return array
     */
    protected function buildItemData($key, array $data, Collection $products)
    {
        if (!$product = $products->find($key)) {
            throw new \RuntimeException("ID $key not found in products collection");
        }

        $this->setProduct($product);

        if (!empty($data['purchased_amount'])) {
            $data['purchased_for'] = $product->in_price;
        }

        if (!empty($data['sold_amount'])) {
            $data['sold_for'] = $product->out_price;
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    protected function beforeCreate($item, array $attributes)
    {
        if ($this->wastage) {
            $item->wastage()->associate($this->wastage);
        }

        if ($this->department) {
            $item->department()->associate($this->department);
        }

        if ($this->product) {
            $item->product()->associate($this->product);
            $this->product = null;
        }
    }
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository\Criteria;

use anlutro\LaravelRepository\CriteriaInterface;

class SimpleCriteria implements CriteriaInterface
{
	/**
	 * @var array
	 */
	protected $wheres = [];

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

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

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

	/**
	 * @param  string $column
	 * @param  string $value
	 * @param  string $operator
	 *
	 * @return void
	 */
	public function where($column, $value, $operator = '=')
	{
		$this->wheres[] = [$column, $operator, $value];
	}

	/**
	 * @param  string $column
	 * @param  string $value
	 * @param  string $operator
	 *
	 * @return void
	 */
	public function orWhere($column, $value, $operator = '=')
	{
		$this->orWheres[] = [$column, $operator, $value];
	}

	/**
	 * @param  string $column
	 * @param  array $values
	 *
	 * @return void
	 */
	public function whereIn($column, array $values)
	{
		$this->whereIns[] = [$column, $values];
	}

	/**
	 * @param string $column
	 * @param array  $values
	 *
	 * @return void
	 */
	public function orWhereIn($column, array $values)
	{
		$this->orWhereIns[] = [$column, $values];
	}

	/**
	 * @param  \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
	 *
	 * @return void
	 */
	public function apply($query)
	{
		$query->where(function($query) {
			foreach (['where', 'orWhere', 'whereIn', 'orWhereIn'] as $method) {
				foreach ($this->{$method.'s'} as $params) {
					call_user_func_array([$query, $method], $params);
				}
			}
		});
	}
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository\Criteria;

use anlutro\LaravelRepository\CriteriaInterface;

class SearchCriteria implements CriteriaInterface
{
	const LOGIC_AND = 1;
	const LOGIC_OR = 2;

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

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

	/**
	 * Whether to use AND or OR.
	 *
	 * @var int
	 */
	protected $logic;

	/**
	 * @param array  $searchableColumns
	 * @param string $searchFor
	 * @param int    $logic
	 */
	public function __construct(array $searchableColumns, $searchFor, $logic = self::LOGIC_AND)
	{
		$this->columns = $searchableColumns;
		$this->search = $searchFor;

		if ($logic !== static::LOGIC_AND && $logic !== static::LOGIC_OR) {
			throw new \InvalidArgumentException('Invalid value for $logic, use one of the SearchCriteria::LOGIC constants');
		}

		$this->logic = $logic;
	}

	/**
	 * @param  \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
	 *
	 * @return void
	 */
	public function apply($query)
	{
		$method = $this->logic === static::LOGIC_AND ? 'where' : 'orWhere';

		$query->where(function($query) use($method) {
			$value = '%'.str_replace(' ', '%', $this->search).'%';
			foreach ($this->columns as $column) {
				$query->$method($column, 'like', $value);
			}
		});
	}
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository\Criteria;

use anlutro\LaravelRepository\CriteriaInterface;

/**
 * Class that stores more criteria with logic conditions.
 *
 * @author  Bradley Weston <b.weston@outlook.com>
 */
class MultipleCriteria implements CriteriaInterface
{
	const LOGIC_AND = 1;
	const LOGIC_OR = 2;

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

	/**
	 * @var CriteriaInterface[]
	 */
	protected $criteria = [];

	/**
	 * Constructor.
	 *
	 * @param  int $logic
	 */
	public function __construct($logic = self::LOGIC_AND)
	{
		if ($logic !== static::LOGIC_AND && $logic !== static::LOGIC_OR) {
			throw new \InvalidArgumentException('Unknown logic argument: '.$logic);
		}

		$this->logic = $logic;
	}

	/**
	 * Add a criteria.
	 *
	 * @param  CriteriaInterface $criteria
	 *
	 * @return $this
	 */
	public function push(CriteriaInterface $criteria)
	{
		$this->criteria[] = $criteria;

		return $this;
	}

	/**
	 * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
	 *
	 * @return void
	 */
	public function apply($query)
	{
		$query->where(function($query) {
			$logic = ($this->logic === static::LOGIC_AND) ? 'and' : 'or';

			foreach ($this->criteria as $criteria) {
				$query->whereNested(function ($query) use ($criteria) {
					$criteria->apply($query);
				}, $logic);
			}
		});
	}
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository;

use anlutro\LaravelValidation\ValidatorInterface;
use Illuminate\Database\Connection;
use Illuminate\Support\Fluent;

/**
 * Abstract database repository pattern. Use it to build repositories that don't
 * utilize Eloquent for simplicity or performance reasons.
 */
abstract class DatabaseRepository extends AbstractRepository
{
	/**
	 * The database connection to use.
	 *
	 * @var \Illuminate\Database\Connection
	 */
	protected $db;

	/**
	 * The table to run queries from.
	 *
	 * @var string
	 */
	protected $table;

	/**
	 * The primary key of the table.
	 *
	 * @var string
	 */
	protected $primaryKey = 'id';

	/**
	 * @param \Illuminate\Database\Connection $db
	 * @param \anlutro\LaravelValidation\ValidatorInterface $validator
	 */
	public function __construct(Connection $db, ValidatorInterface $validator = null)
	{
		if ($this->table === null) {
			$class = get_class($this);
			throw new \RuntimeException("Property {$class}::\$table must be defined.");
		}

		parent::__construct($validator);

		$this->setConnection($db);

		if ($validator) {
			$validator->replace('table', $this->table);
		}
	}

	/**
	 * Set the connection to run queries on.
	 *
	 * @param \Illuminate\Database\Connection $db
	 *
	 * @return $this
	 */
	public function setConnection(Connection $db)
	{
		$this->db = $db;

		return $this;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getConnection()
	{
		return $this->db;
	}

	/**
	 * Set the table to query from.
	 *
	 * @param  string $table
	 *
	 * @return $this
	 */
	public function setTable($table)
	{
		$this->table = (string) $table;

		return $this;
	}

	/**
	 * Get the table that's being queried from.
	 *
	 * @return string
	 */
	public function getTable()
	{
		return $this->table;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getKeyName()
	{
		return "{$this->table}.{$this->primaryKey}";
	}

	/**
	 * {@inheritdoc}
	 *
	 * @return  \Illuminate\Support\Fluent
	 */
	public function getNew(array $attributes = array())
	{
		return new Fluent($attributes);
	}

	/**
	 * {@inheritdoc}
	 *
	 * @return  \Illuminate\Support\Fluent|false
	 */
	protected function performCreate($entity, array $attributes = array())
	{
		$this->fillEntityAttributes($entity, $attributes);

		$result = $this->newQuery()
			->insert($this->getEntityAttributes($entity));

		return $result ? $entity : false;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function performUpdate($entity, array $attributes)
	{
		$this->fillEntityAttributes($entity, $attributes);

		return (bool) $this->newQuery()
			->where($this->getKeyName(), '=', $this->getEntityKey($entity))
			->update($this->getEntityAttributes($entity));
	}

	/**
	 * {@inheritdoc}
	 */
	protected function performDelete($entity)
	{
		return (bool) $this->newQuery()
			->where($this->getKeyName(), '=', $this->getEntityKey($entity))
			->delete();
	}

	/**
	 * {@inheritdoc}
	 */
	protected function newQuery()
	{
		return $this->db->table($this->table);
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getEntityKey($entity)
	{
		return $entity->{$this->primaryKey};
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getEntityAttributes($entity)
	{
		return $entity->getAttributes();
	}

	/**
	 * Fill an entity's attributes.
	 *
	 * @param  mixed  $entity
	 * @param  array  $attributes
	 *
	 * @return void
	 */
	protected function fillEntityAttributes($entity, array $attributes)
	{
		foreach ($attributes as $key => $value) {
			$entity->$key = $value;
		}
	}
}


<?php
/**
 * Laravel 4 Repository classes
 *
 * @author   Andreas Lutro <anlutro@gmail.com>
 * @license  http://opensource.org/licenses/MIT
 * @package  l4-repository
 */

namespace anlutro\LaravelRepository;

use Closure;
use Illuminate\Support\MessageBag;
use anlutro\LaravelValidation\ValidatorInterface;

/**
 * Abstract repository class.
 */
abstract class AbstractRepository
{
	/**
	 * Whether or not to throw exceptions or return null when "find" methods do
	 * not yield any results.
	 *
	 * @var boolean
	 */
	protected $throwExceptions = false;

	/**
	 * Whether or not to paginate results.
	 *
	 * @var boolean
	 */
	protected $paginate = false;

	/**
	 * @var \anlutro\LaravelValidation\ValidatorInterface
	 */
	protected $validator;

	/**
	 * Whether to validate the model or the input attributes.
	 *
	 * @var boolean
	 */
	protected $validateEntity = false;

	/**
	 * The currently active criteria.
	 *
	 * @var \anlutro\LaravelRepository\CriteriaInterface[]
	 */
	protected $criteria = [];

	/**
	 * Criteria classes that should be applied to every query.
	 *
	 * This array will be converted to an array of Criteria objects when the
	 * repository is instantiated.
	 *
	 * @var string[]|\anlutro\LaravelRepository\CriteriaInterface[]
	 */
	protected $defaultCriteria = [];

	/**
	 * Whether or not criteria should be reset on each query.
	 *
	 * @var boolean
	 */
	protected $resetCriteria = true;

	/**
	 * @var \Illuminate\Support\MessageBag
	 */
	protected $errors;

	/**
	 * @param \anlutro\LaravelValidation\ValidatorInterface $validator Optional
	 */
	public function __construct(ValidatorInterface $validator = null)
	{
		$this->resetErrors();

		$this->setupDefaultCriteria();

		if ($validator) {
			$this->setValidator($validator);
		}
	}

	/**
	 * Set up the repository's default criteria.
	 *
	 * @return void
	 */
	protected function setupDefaultCriteria()
	{
		$defaultCriteria = $this->defaultCriteria;
		$this->defaultCriteria = [];

		foreach ($defaultCriteria as $criteria) {
			$this->addDefaultCriteria(new $criteria);
		}
	}

	/**
	 * Add a default criteria to the repository. Default criteria are applied to
	 * every query.
	 *
	 * @param \anlutro\LaravelRepository\CriteriaInterface $criteria
	 *
	 * @return $this
	 */
	protected function addDefaultCriteria(CriteriaInterface $criteria)
	{
		$this->defaultCriteria[] = $criteria;

		return $this;
	}

	/**
	 * Perform an action.
	 *
	 * Calls $this->before{$action} and $this->after{$action} before or after
	 * $this->perform{action} has been called. Also calls $this->valid($action,
	 * $attributes) for validation.
	 *
	 * @param  string  $action
	 * @param  mixed   $object
	 * @param  mixed   $attributes
	 * @param  boolean $validate
	 *
	 * @return mixed
	 */
	protected function perform($action, $object, $attributes = array(), $validate = true)
	{
		$perform = 'perform' . ucfirst($action);
		if (!method_exists($this, $perform)) {
			throw new \BadMethodCallException("Method $perform does not exist on this class");
		}

		if ($validate === true) {
			if ($this->validateEntity) {
				if (!$this->valid($action, $this->getEntityAttributes($object))) return false;
			} else {
				if (!$this->valid($action, $attributes)) return false;
			}
		}

		$beforeResult = $this->doBefore($action, $object, $attributes);
		if ($beforeResult === false) return $beforeResult;

		$result = call_user_func_array([$this, $perform], [$object, $attributes]);
		if ($result === false) return $result;

		$this->doAfter($action, $result, $attributes);

		return $result;
	}

	/**
	 * Perform a before or after action.
	 *
	 * @param  string $which  before or after
	 * @param  string $action
	 * @param  array  $args
	 *
	 * @return false|null
	 */
	protected function doBeforeOrAfter($which, $action, array $args)
	{
		$method = $which.ucfirst($action);

		if (method_exists($this, $method)) {
			$result = call_user_func_array([$this, $method], $args);
			if ($result === false) return $result;
		}

		return null;
	}

	/**
	 * Validate a set of attributes against a certain action.
	 *
	 * @param  string $action
	 * @param  array  $attributes
	 *
	 * @return boolean
	 */
	public function valid($action, array $attributes)
	{
		if ($this->validator === null) {
			return true;
		}

		$result = $this->validator->valid($action, $attributes);

		if ($result === false) {
			$this->errors->merge($this->validator->getErrors());
		}

		return $result;
	}

	/**
	 * Perform a query.
	 *
	 * @param  mixed   $query
	 * @param  boolean $many
	 *
	 * @return mixed
	 */
	protected function performQuery($query, $many)
	{
		$this->applyCriteria($query);

		if ($many === false) {
			$result = $this->getRegularQueryResults($query, false);

			if (!$result && $this->throwExceptions === true) {
				throw $this->getNotFoundException($query);
			}

			return $result;
		}

		return $this->paginate === false ?
			$this->getRegularQueryResults($query, true) :
			$this->getPaginatedQueryResults($query);
	}

	/**
	 * Get a new "not found" exception.
	 *
	 * @param  mixed $query
	 *
	 * @return \Exception
	 */
	protected function getNotFoundException($query)
	{
		return new NotFoundException();
	}

	/**
	 * Get regular results from a query builder.
	 *
	 * @param  mixed   $query
	 * @param  boolean $many
	 *
	 * @return mixed
	 */
	protected function getRegularQueryResults($query, $many)
	{
		return $many ? $query->get() : $query->first();
	}

	/**
	 * Get paginated results from a query.
	 *
	 * @param  mixed $query
	 *
	 * @return mixed
	 */
	protected function getPaginatedQueryResults($query)
	{
		return $query->paginate($this->paginate);
	}

	/**
	 * Reset the repository's errors.
	 *
	 * @return $this
	 */
	protected function resetErrors()
	{
		$this->errors = new MessageBag;

		return $this;
	}

	/**
	 * Get the repository's error messages.
	 *
	 * @return \Illuminate\Support\MessageBag
	 */
	public function getErrors()
	{
		return $this->errors;
	}

	/**
	 * Set the repository's validator.
	 *
	 * @param \anlutro\LaravelValidation\ValidatorInterface $validator
	 *
	 * @return $this
	 */
	public function setValidator(ValidatorInterface $validator)
	{
		$this->validator = $validator;

		return $this;
	}

	/**
	 * Get the repository's validator.
	 *
	 * @return \anlutro\LaravelValidation\ValidatorInterface
	 */
	public function getValidator()
	{
		return $this->validator;
	}

	/**
	 * Toggle pagination.
	 *
	 * @param  false|int $toggle
	 *
	 * @return $this
	 */
	public function paginate($toggle)
	{
		$this->paginate = $toggle === false ? false : (int) $toggle;

		return $this;
	}

	/**
	 * Toggle throwing of exceptions.
	 *
	 * @param  boolean $toggle
	 * @param  boolean $toggleValidator Whether or not to toggle exceptions on
	 * the validator as well as the repository. Defaults to true
	 *
	 * @return $this
	 */
	public function toggleExceptions($toggle, $toggleValidator = true)
	{
		$this->throwExceptions = (bool) $toggle;

		if ($this->validator && $toggleValidator) {
			$this->validator->toggleExceptions((bool) $toggle);
		}

		return $this;
	}

	/**
	 * Do a before action.
	 *
	 * @see   doBeforeOrAfter
	 *
	 * @param  string $action
	 * @param  object $object
	 * @param  mixed  $attributes
	 *
	 * @return mixed
	 */
	protected function doBefore($action, $object, $attributes)
	{
		return $this->doBeforeOrAfter('before', $action, [$object, $attributes]);
	}

	/**
	 * Do an after action.
	 *
	 * @see   doBeforeOrAfter
	 *
	 * @param  string $action
	 * @param  mixed  $result
	 * @param  mixed  $attributes
	 *
	 * @return mixed
	 */
	protected function doAfter($action, $result, $attributes)
	{
		return $this->doBeforeOrAfter('after', $action, [$result, $attributes]);
	}

	/**
	 * Add a criteria to the current stack.
	 *
	 * @param  \anlutro\LaravelRepository\CriteriaInterface $criteria
	 *
	 * @return $this
	 */
	public function pushCriteria(CriteriaInterface $criteria)
	{
		$this->criteria[] = $criteria;

		return $this;
	}

	/**
	 * Reset the criteria stack.
	 *
	 * @return $this
	 */
	public function resetCriteria()
	{
		$this->criteria = [];

		return $this;
	}

	/**
	 * Apply the repository's criteria onto a query builder.
	 *
	 * @param  mixed $query
	 *
	 * @return void
	 */
	public function applyCriteria($query)
	{
		foreach ($this->defaultCriteria as $criteria) {
			$criteria->apply($query);
		}

		if (empty($this->criteria)) return;

		foreach ($this->criteria as $criteria) {
			$criteria->apply($query);
		}

		if ($this->resetCriteria) {
			$this->resetCriteria();
		}
	}

	/**
	 * Create and persist a new entity with the given attributes.
	 *
	 * @param  array  $attributes
	 *
	 * @return object|false
	 */
	public function create(array $attributes)
	{
		return $this->perform('create', $this->getNew(), $attributes, true);
	}

	/**
	 * Update an entity with the given attributes and persist it.
	 *
	 * @param  object  $entity
	 * @param  array   $attributes
	 *
	 * @return boolean
	 */
	public function update($entity, array $attributes)
	{
		if ($this->validator) {
			$this->validator->replace('key', $this->getEntityKey($entity));
		}

		return $this->perform('update', $entity, $attributes, true) ? true : false;
	}

	/**
	 * Delete an entity.
	 *
	 * @param  object  $entity
	 *
	 * @return boolean
	 */
	public function delete($entity)
	{
		return $this->perform('delete', $entity, [], false);
	}

	/**
	 * Perform a query, fetching multiple rows.
	 *
	 * @param  \Illuminate\Database\Query\Builder  $query
	 *
	 * @return mixed
	 */
	protected function fetchMany($query)
	{
		return $this->perform('query', $query, true, false);
	}

	/**
	 * Perform a query, fetching a single row.
	 *
	 * @param  \Illuminate\Database\Query\Builder  $query
	 *
	 * @return mixed
	 */
	protected function fetchSingle($query)
	{
		return $this->perform('query', $query, false, false);
	}

	/**
	 * Perform a query, fetching an array of columns.
	 *
	 * @param  \Illuminate\Database\Query\Builder  $query
	 * @param  string $column
	 * @param  string $key    Column to be used as the array keys
	 *
	 * @return array
	 */
	protected function fetchList($query, $column = 'id', $key = null)
	{
		$this->doBefore('query', $query, true);
		$this->applyCriteria($query);

		return $query->lists($column, $key);
	}

	/**
	 * Get a specific row by key in the repository.
	 *
	 * @param  mixed $key
	 *
	 * @return mixed
	 */
	public function findByKey($key)
	{
		$query = $this->newQuery()
			->where($this->getKeyName(), '=', $key);

		return $this->fetchSingle($query);
	}

	/**
	 * Get a specific row by attributes in the repository.
	 *
	 * @param  array $attributes
	 *
	 * @return mixed
	 */
	public function findByAttributes(array $attributes)
	{
		$query = $this->newAttributesQuery($attributes);

		if (empty($attributes)) {
			if ($this->throwExceptions) {
				throw $this->getNotFoundException($query);
			}

			return null;
		}

		return $this->fetchSingle($query);
	}

	/**
	 * Get a specific row by criteria.
	 *
	 * @param  \anlutro\LaravelRepository\CriteriaInterface $criteria
	 *
	 * @return mixed
	 */
	public function findByCriteria(CriteriaInterface $criteria)
	{
		$this->resetCriteria();
		$this->pushCriteria($criteria);

		return $this->fetchSingle($this->newQuery());
	}

	/**
	 * Get all rows.
	 *
	 * @return mixed
	 */
	public function getAll()
	{
		$query = $this->newQuery();

		return $this->fetchMany($query);
	}

	/**
	 * Get a specific row by attributes in the repository.
	 *
	 * @param  array $attributes
	 *
	 * @return mixed
	 *
	 * @throws \InvalidArgumentException if $attributes is empty
	 */
	public function getByAttributes(array $attributes)
	{
		if (count($attributes) < 1) {
			throw new \InvalidArgumentException('Cannot getByAttributes with an empty array');
		}

		return $this->fetchMany($this->newAttributesQuery($attributes));
	}

	/**
	 * Get a collection of rows by a criteria. This resets all previously pushed
	 * criteria.
	 *
	 * @param  \anlutro\LaravelRepository\CriteriaInterface $criteria
	 *
	 * @return mixed
	 */
	public function getByCriteria(CriteriaInterface $criteria)
	{
		$this->resetCriteria();
		$this->pushCriteria($criteria);

		return $this->fetchMany($this->newQuery());
	}

	/**
	 * Get a collection of rows by an array of primary keys.
	 *
	 * @param  array  $keys
	 *
	 * @return mixed
	 *
	 * @throws \InvalidArgumentException if $keys is empty
	 */
	public function getByKeys(array $keys)
	{
		if (count($keys) < 1) {
			throw new \InvalidArgumentException('Cannot getByKeys with an empty array');
		}

		$query = $this->newQuery()
			->whereIn($this->getKeyName(), $keys);

		return $this->fetchMany($query);
	}

	/**
	 * Get a list of columns from the repository.
	 *
	 * @see    \Illuminate\Database\Query::lists()
	 *
	 * @param  string $column
	 * @param  string $key    Column to be used as the array keys
	 *
	 * @return array
	 */
	public function getList($column = 'id', $key = null)
	{
		return $this->fetchList($this->newQuery(), $column, $key);
	}

	/**
	 * Perform a database transaction.
	 *
	 * The Connection object will be passed as a first argument to the closure.
	 *
	 * @param  \Closure $closure
	 *
	 * @return mixed
	 */
	protected function transaction(Closure $closure)
	{
		return $this->getConnection()->transaction($closure);
	}

	/**
	 * Get a new query that searches by attributes.
	 *
	 * @param  array  $attributes
	 * @param  string $operator   Default: '='
	 *
	 * @return mixed
	 */
	protected function newAttributesQuery(array $attributes, $operator = '=')
	{
		$query = $this->newQuery();

		foreach ($attributes as $key => $value) {
			$query->where($key, $operator, $value);
		}

		return $query;
	}

	/**
	 * Get the connection the repository uses.
	 *
	 * @return \Illuminate\Database\Connection
	 */
	protected abstract function getConnection();

	/**
	 * Get a new query builder instance.
	 *
	 * @return \Illuminate\Database\Query\Builder
	 */
	protected abstract function newQuery();

	/**
	 * Get a new entity instance.
	 *
	 * @param  array  $attributes
	 *
	 * @return object
	 */
	public abstract function getNew(array $attributes = array());

	/**
	 * Perform a create action.
	 *
	 * @param  object  $entity
	 * @param  array   $attributes
	 *
	 * @return object|false
	 */
	protected abstract function performCreate($entity, array $attributes);

	/**
	 * Perform an update action.
	 *
	 * @param  object  $entity
	 * @param  array   $attributes
	 *
	 * @return boolean
	 */
	protected abstract function performUpdate($entity, array $attributes);

	/**
	 * Perform a delete action.
	 *
	 * @param  object  $entity
	 *
	 * @return boolean
	 */
	protected abstract function performDelete($entity);

	/**
	 * Get the name of the primary key to query for.
	 *
	 * @return string
	 */
	protected abstract function getKeyName();

	/**
	 * Get the primary key of an entity.
	 *
	 * @param  object  $entity
	 *
	 * @return mixed
	 */
	protected abstract function getEntityKey($entity);

	/**
	 * Get an entity's attributes.
	 *
	 * @param  object  $entity
	 *
	 * @return mixed
	 */
	protected abstract function getEntityAttributes($entity);
}