<?php
/*
 * This file is part of JoliCi.
 *
 * (c) Joel Wurtz <jwurtz@jolicode.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Joli\JoliCi\BuildStrategy;

use Joli\JoliCi\Job;
use Joli\JoliCi\Builder\DockerfileBuilder;
use Joli\JoliCi\Filesystem\Filesystem;
use Joli\JoliCi\Matrix;
use Joli\JoliCi\Naming;
use Joli\JoliCi\Service;
use Symfony\Component\Yaml\Yaml;

/**
 * TravisCi implementation for build strategy
 *
 * A project must have a .travis.yml file
 *
 * @author Joel Wurtz <jwurtz@jolicode.com>
 */
class TravisCiBuildStrategy implements BuildStrategyInterface
{
    private $languageVersionKeyMapping = array(
        'ruby' => 'rvm',
    );

    private $defaults = array(
        'php' => array(
            'before_install' => array(),
            'install'        => array(),
            'before_script'  => array(),
            'script'         => array('phpunit'),
            'env'            => array(),
            'default_versions' => array('5.6')
        ),
        'ruby' => array(
            'before_install' => array(),
            'install'        => array(),
            'before_script'  => array(),
            'script'         => array('bundle exec rake'),
            'env'            => array(),
            'default_versions' => array('2.1.0')
        ),
        'node_js' => array(
            'before_install' => array(),
            'install'        => array(),
            'before_script'  => array(),
            'script'         => array('npm test'),
            'env'            => array(),
            'default_versions' => array('0.10')
        ),
    );

    private $servicesMapping = array(
        'mongodb' => array(
            'repository' => 'mongo',
            'tag' => '2.6',
            'config' => array()
        ),
        'mysql'   => array(
            'repository' => 'mysql',
            'tag' => '5.5',
            'config' => array(
                'Env' => array(
                    'MYSQL_ROOT_PASSWORD=""',
                    'MYSQL_USER=travis',
                    'MYSQL_PASSWORD=""'
                )
            )
        ),
        'postgresql' => array(
            'repository' => 'postgres',
            'tag'        => '9.1',
            'config'     => array()
        ),
        'couchdb' => array(
            'repository' => 'fedora/couchdb',
            'tag' => 'latest',
            'config' => array()
        ),
        'rabbitmq' => array(
            'repository' => 'dockerfile/rabbitmq',
            'tag' => 'latest',
            'config' => array()
        ),
        'memcached' => array(
            'repository' => 'sylvainlasnier/memcached',
            'tag' => 'latest',
            'config' => array()
        ),
        'redis-server' => array(
            'repository' => 'redis',
            'tag' => '2.8',
            'config' => array()
        ),
        'cassandra' => array(
            'repository' => 'spotify/cassandra',
            'tag' => 'latest',
            'config' => array()
        ),
        'neo4j' => array(
            'repository' => 'tpires/neo4j',
            'tag' => 'latest',
            'config' => array()
        ),
        'elasticsearch' => array(
            'repository' => 'dockerfile/elasticsearch',
            'tag' => 'latest',
            'config' => array()
        ),
    );

    /**
     * @var DockerfileBuilder Builder for dockerfile
     */
    private $builder;

    /**
     * @var string Build path for project
     */
    private $buildPath;

    /**
     * @var Filesystem Filesystem service
     */
    private $filesystem;

    /**
     * @var \Joli\JoliCi\Naming Naming service to create docker name for images
     */
    private $naming;

    /**
     * @param DockerfileBuilder $builder    Twig Builder for Dockerfile
     * @param string            $buildPath  Directory where builds are created
     * @param Naming            $naming     Naming service
     * @param Filesystem        $filesystem Filesystem service
     */
    public function __construct(DockerfileBuilder $builder, $buildPath, Naming $naming, Filesystem $filesystem)
    {
        $this->builder    = $builder;
        $this->buildPath  = $buildPath;
        $this->naming     = $naming;
        $this->filesystem = $filesystem;
    }

    /**
     * {@inheritdoc}
     */
    public function getJobs($directory)
    {
        $jobs       = array();
        $config     = Yaml::parse(file_get_contents($directory.DIRECTORY_SEPARATOR.".travis.yml"));
        $matrix     = $this->createMatrix($config);
        $services   = $this->getServices($config);
        $timezone   = ini_get('date.timezone');

        foreach ($matrix->compute() as $possibility) {
            $parameters   = array(
                'language' => $possibility['language'],
                'version' => $possibility['version'],
                'environment' => $possibility['environment'],
            );

            $description = sprintf('%s = %s', $possibility['language'], $possibility['version']);

            if ($possibility['environment'] !== null) {
                $description .= sprintf(', Environment: %s', json_encode($possibility['environment']));
            }

            $jobs[] = new Job($this->naming->getProjectName($directory), $this->getName(), $this->naming->getUniqueKey($parameters), array(
                'language'       => $possibility['language'],
                'version'        => $possibility['version'],
                'before_install' => $possibility['before_install'],
                'install'        => $possibility['install'],
                'before_script'  => $possibility['before_script'],
                'script'         => $possibility['script'],
                'env'            => $possibility['environment'],
                'global_env'     => $possibility['global_env'],
                'timezone'       => $timezone,
                'origin'         => realpath($directory),
            ), $description, null, $services);
        }

        return $jobs;
    }

    /**
     * {@inheritdoc}
     */
    public function prepareJob(Job $job)
    {
        $parameters = $job->getParameters();
        $origin     = $parameters['origin'];
        $target     = $this->buildPath.DIRECTORY_SEPARATOR. $job->getDirectory();

        // First mirroring target
        $this->filesystem->mirror($origin, $target, null, array(
            'delete' => true,
            'override' => true,
        ));

        // Create dockerfile
        $this->builder->setTemplateName(sprintf("%s/Dockerfile-%s.twig", $parameters['language'], $parameters['version']));
        $this->builder->setVariables($parameters);
        $this->builder->setOutputName('Dockerfile');
        $this->builder->writeOnDisk($target);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return "TravisCi";
    }

    /**
     * {@inheritdoc}
     */
    public function supportProject($directory)
    {
        return file_exists($directory.DIRECTORY_SEPARATOR.".travis.yml") && is_file($directory.DIRECTORY_SEPARATOR.".travis.yml");
    }

    /**
     * Get command lines to add for a configuration value in .travis.yml file
     *
     * @param array  $config   Configuration of travis ci parsed
     * @param string $language Language for getting the default value if no value is set
     * @param string $key      Configuration key
     *
     * @return array A list of command to add to Dockerfile
     */
    private function getConfigValue($config, $language, $key)
    {
        if (!isset($config[$key]) || empty($config[$key])) {
            if (isset($this->defaults[$language][$key])) {
                return $this->defaults[$language][$key];
            }

            return array();
        }

        if (!is_array($config[$key])) {
            return array($config[$key]);
        }

        return $config[$key];
    }

    /**
     * Create matrix of build
     *
     * @param array $config
     *
     * @return Matrix
     */
    protected function createMatrix($config)
    {
        $language = isset($config['language']) ? $config['language'] : 'ruby';

        if (!isset($this->defaults[$language])) {
            throw new \Exception(sprintf('Language %s not supported', $language));
        }

        $versionKey       = isset($this->languageVersionKeyMapping[$language]) ? $this->languageVersionKeyMapping[$language] : $language;
        $environmentLines = $this->getConfigValue($config, $language, "env");
        $environnements   = array();
        $globalEnv        = array();
        $matrixEnv        = $environmentLines;
        $versions         = (array) (isset($config[$versionKey]) ? $config[$versionKey] : $this->defaults[$language]['default_versions']);

        foreach ($versions as $key => $version) {
            if (!$this->isLanguageVersionSupported($language, $version)) {
                unset($versions[$key]);
            }
        }

        if (isset($environmentLines['matrix'])) {
            $matrixEnv = $environmentLines['matrix'];
        }

        if (isset($environmentLines['global'])) {
            foreach ($environmentLines['global'] as $environementVariable) {
                if (is_array($environementVariable) && array_key_exists('secure', $environementVariable)) {
                    continue;
                }

                list ($key, $value) = $this->parseEnvironementVariable($environementVariable);
                $globalEnv = array_merge($globalEnv, array($key => $value));
            }

            if (!isset($environmentLines['matrix'])) {
                $matrixEnv = array();
            }
        }

        // Parsing environnements
        foreach ($matrixEnv as $environmentLine) {
            $environnements[] = $this->parseEnvironmentLine($environmentLine);
        }

        $matrix = new Matrix();
        $matrix->setDimension('language', array($language));
        $matrix->setDimension('environment', $environnements);
        $matrix->setDimension('global_env', array($globalEnv));
        $matrix->setDimension('version', $versions);
        $matrix->setDimension('before_install', array($this->getConfigValue($config, $language, 'before_install')));
        $matrix->setDimension('install', array($this->getConfigValue($config, $language, 'install')));
        $matrix->setDimension('before_script', array($this->getConfigValue($config, $language, 'before_script')));
        $matrix->setDimension('script', array($this->getConfigValue($config, $language, 'script')));

        return $matrix;
    }

    /**
     * Get services list from travis ci configuration file
     *
     * @param $config
     *
     * @return Service[]
     */
    protected function getServices($config)
    {
        $services       = array();
        $travisServices = isset($config['services']) && is_array($config['services']) ? $config['services'] : array();

        foreach ($travisServices as $service) {
            if (isset($this->servicesMapping[$service])) {
                $services[] = new Service(
                    $service,
                    $this->servicesMapping[$service]['repository'],
                    $this->servicesMapping[$service]['tag'],
                    $this->servicesMapping[$service]['config']
                );
            }
        }

        return $services;
    }

    /**
     * Parse an environnement line from Travis to return an array of variables
     *
     * Transform:
     *   "A=B C=D"
     * Into:
     *   array('a' => 'b', 'c' => 'd')
     *
     * @param $environmentLine
     * @return array
     */
    private function parseEnvironmentLine($environmentLine)
    {
        $variables     = array();@
        $variableLines = explode(' ', $environmentLine ?: '');

        foreach ($variableLines as $variableLine) {
            if (!empty($variableLine)) {
                list($key, $value) = $this->parseEnvironementVariable($variableLine);
                $variables[$key]   = $value;
            }
        }

        return $variables;
    }

    /**
     * Parse an envar
     *
     * @param $envVar
     * @return array<Key, Value>
     */
    private function parseEnvironementVariable($envVar)
    {
        return explode('=', $envVar);
    }

    private function isLanguageVersionSupported($language, $version)
    {
        return file_exists(__DIR__
            . DIRECTORY_SEPARATOR . '..'
            . DIRECTORY_SEPARATOR . '..'
            . DIRECTORY_SEPARATOR . '..'
            . DIRECTORY_SEPARATOR . '..'
            . DIRECTORY_SEPARATOR . 'resources'
            . DIRECTORY_SEPARATOR . 'templates'
            . DIRECTORY_SEPARATOR . $language
            . DIRECTORY_SEPARATOR . 'Dockerfile-' . $version . '.twig');
    }
}


<?php

namespace Joli\JoliCi;

use Docker\API\Model\ImageItem;
use Docker\Docker;
use Docker\Image;
use Joli\JoliCi\BuildStrategy\BuildStrategyInterface;
use Joli\JoliCi\Filesystem\Filesystem;

class Vacuum
{
    /**
     * @var \Docker\Docker Docker API Client
     */
    private $docker;

    /**
     * @var Naming Service use to create name for image and project
     */
    private $naming;

    /**
     * @var BuildStrategy\BuildStrategyInterface Strategy where we want to clean the builds
     */
    private $strategy;

    /**
     * @var string Location where the build are
     */
    private $buildPath;

    /**
     * @var \Joli\JoliCi\Filesystem\Filesystem
     */
    private $filesystem;

    /**
     * @param Docker                 $docker     Docker API Client
     * @param Naming                 $naming     Naming service
     * @param BuildStrategyInterface $strategy   Strategy used to create builds
     * @param Filesystem             $filesystem Filesystem service
     * @param string                 $buildPath  Directory where builds are created
     */
    public function __construct(Docker $docker, Naming $naming, BuildStrategyInterface $strategy, Filesystem $filesystem, $buildPath)
    {
        $this->docker     = $docker;
        $this->naming     = $naming;
        $this->strategy   = $strategy;
        $this->buildPath  = $buildPath;
        $this->filesystem = $filesystem;
    }

    /**
     * Clean containers, images and directory from a project
     *
     * @param string  $projectPath Location of the project
     * @param int     $keep        How many versions does we need to keep (1 is the default in order to have cache for each test)
     * @param boolean $force       Force removal for images
     */
    public function clean($projectPath, $keep = 1, $force = false)
    {
        $builds = $this->getJobsToRemove($projectPath, $keep);

        $this->cleanDirectories($builds);
        $this->cleanContainers($builds);
        $this->cleanImages($builds, $force);
    }

    /**
     * Clean directories for given builds
     *
     * @param \Joli\JoliCi\Job[] $jobs A list of jobs to remove images from
     */
    public function cleanDirectories($jobs = array())
    {
        foreach ($jobs as $job) {
            $this->filesystem->remove($job->getDirectory());
        }
    }

    /**
     * Clean images for given builds
     *
     * @param \Joli\JoliCi\Job[] $jobs A list of jobs to remove images from
     */
    public function cleanContainers($jobs = array())
    {
        $images     = array();
        $containers = array();

        foreach ($jobs as $job) {
            if (isset($job->getParameters()['image'])) {
                $images[] = $job->getParameters()['image'];
            } else {
                $images[] = sprintf('%s:%s', $job->getRepository(), $job->getTag());
            }
        }

        foreach ($this->docker->getContainerManager()->findAll(['all' => 1]) as $container) {
            $imageName = $container->getImage();

            foreach ($images as $image) {
                if ($image == $imageName) {
                    $containers[] = $container;
                }

                if (preg_match('#^'.$imageName.'#', $image->getId())) {
                    $containers[] = $container;
                }
            }
        }

        foreach ($containers as $container) {
            $this->docker->getContainerManager()->remove($container->getId(), [
                'v' => true,
                'force' => true
            ]);
        }
    }

    /**
     * Clean images for given builds
     *
     * @param \Joli\JoliCi\Job[] $jobs   A list of jobs to remove images from
     * @param boolean            $force  Force removal for images
     */
    public function cleanImages($jobs = array(), $force = false)
    {
        foreach ($jobs as $job) {
            $this->docker->getImageManager()->remove(sprintf('%s:%s', $job->getRepository(), $job->getTag()), [
                'force' => $force
            ]);
        }
    }

    /**
     * Get all jobs to remove given a project and how many versions to keep
     *
     * @param string $projectPath The project path
     * @param int    $keep        Number of project to keep
     *
     * @return \Joli\JoliCi\Job[] A list of jobs to remove
     */
    public function getJobsToRemove($projectPath, $keep = 1)
    {
        $currentJobs  = $this->strategy->getJobs($projectPath);
        $existingJobs = $this->getJobs($projectPath);
        $uniqList = array();
        $removes  = array();
        $ordered  = array();

        foreach ($currentJobs as $job) {
            $uniqList[] = $job->getUniq();
        }

        // Remove not existant image (old build configuration)
        foreach ($existingJobs as $job) {
            if (!in_array($job->getUniq(), $uniqList)) {
                $removes[] = $job;
            } else {
                $ordered[$job->getUniq()][$job->getCreated()->format('U')] = $job;
            }
        }

        // Remove old image
        foreach ($ordered as $jobs) {
            ksort($jobs);
            $keeped = count($jobs);

            while ($keeped > $keep) {
                $removes[] = array_shift($jobs);
                $keeped--;
            }
        }

        return $removes;
    }

    /**
     * Get all jobs related to a project
     *
     * @param string $projectPath Directory where the project is
     *
     * @return \Joli\JoliCi\Job[]
     */
    protected function getJobs($projectPath)
    {
        $jobs            = array();
        $project         = $this->naming->getProjectName($projectPath);
        $repositoryRegex = sprintf('#^%s_([a-z]+?)/%s:\d+-\d+$#', Job::BASE_NAME, $project);

        foreach ($this->docker->getImageManager()->findAll() as $image) {
            foreach ($image->getRepoTags() as $name) {
                if (preg_match($repositoryRegex, $name, $matches)) {
                    $jobs[] = $this->getJobFromImage($image, $name, $matches[1], $project);
                }
            }
        }

        return $jobs;
    }

    /**
     * Create a job from a docker image
     *
     * @param ImageItem $image
     * @param string    $strategy
     * @param string    $project
     *
     * @return \Joli\JoliCi\Job
     */
    protected function getJobFromImage(ImageItem $image, $imageName, $strategy, $project)
    {
        $tag = explode(':', $imageName)[1];
        list($uniq, $timestamp)     = explode('-', $tag);

        return new Job($project, $strategy, $uniq, array('image' => $image), "", \DateTime::createFromFormat('U', $timestamp));
    }
}


<?php
/*
 * This file is part of JoliCi.
*
* (c) Joel Wurtz <jwurtz@jolicode.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliCi;

use Docker\API\Model\BuildInfo;
use Docker\API\Model\ContainerConfig;
use Docker\API\Model\HostConfig;
use Docker\Docker;
use Docker\Context\Context;
use Docker\Manager\ContainerManager;
use Docker\Manager\ImageManager;
use Docker\Stream\BuildStream;
use Http\Client\Plugin\Exception\ClientErrorException;
use Monolog\Logger;

class Executor
{
    /**
     * Docker client
     *
     * @var Docker
     */
    protected $docker;

    /**
     * Logger to log message when building
     *
     * @var LoggerCallback
     */
    protected $logger;

    /**
     * @var boolean Use cache when building
     */
    private $usecache = true;

    /**
     * @var boolean Use cache when building
     */
    private $quietBuild = true;

    /**
     * @var integer Default timeout for run
     */
    private $timeout = 600;

    /**
     * @var string Base directory where builds are located
     */
    private $buildPath;

    public function __construct(LoggerCallback $logger, Docker $docker, $buildPath, $usecache = true, $quietBuild = true, $timeout = 600)
    {
        $this->logger     = $logger;
        $this->docker     = $docker;
        $this->usecache   = $usecache;
        $this->quietBuild = $quietBuild;
        $this->timeout    = $timeout;
        $this->buildPath  = $buildPath;
    }

    /**
     * Test a build
     *
     * @param Job $build
     * @param array|string $command
     *
     * @return integer
     */
    public function test(Job $build, $command = null)
    {
        $exitCode = 1;

        if (false !== $this->create($build)) {
            $exitCode = $this->run($build, $command);
        }

        return $exitCode;
    }

    /**
     * Create a build
     *
     * @param Job $job Build used to create image
     *
     * @return \Docker\API\Model\Image|boolean Return the image created if successful or false otherwise
     */
    public function create(Job $job)
    {
        $context  = new Context($this->buildPath . DIRECTORY_SEPARATOR . $job->getDirectory());

        $buildStream = $this->docker->getImageManager()->build($context->toStream(), [
            't' => $job->getName(),
            'q' => $this->quietBuild,
            'nocache' => !$this->usecache
        ], ImageManager::FETCH_STREAM);

        $buildStream->onFrame($this->logger->getBuildCallback());
        $buildStream->wait();

        try {
            return $this->docker->getImageManager()->find($job->getName());
        } catch (ClientErrorException $e) {
            if ($e->getResponse()->getStatusCode() == 404) {
                return false;
            }

            throw $e;
        }
    }

    /**
     * Run a build (it's suppose the image exist in docker
     *
     * @param Job $job Build to run
     * @param string|array $command Command to use when run the build (null, by default, will use the command registered to the image)
     *
     * @return integer The exit code of the command run inside (0 = success, otherwise it has failed)
     */
    public function run(Job $job, $command)
    {
        if (is_string($command)) {
            $command = ['/bin/bash', '-c', $command];
        }

        $image = $this->docker->getImageManager()->find($job->getName());

        $hostConfig = new HostConfig();

        $config = new ContainerConfig();
        $config->setCmd($command);
        $config->setImage($image->getId());
        $config->setHostConfig($hostConfig);
        $config->setLabels(new \ArrayObject([
            'com.jolici.container=true'
        ]));
        $config->setAttachStderr(true);
        $config->setAttachStdout(true);

        $links = [];

        foreach ($job->getServices() as $service) {
            if ($service->getContainer()) {
                $serviceContainer = $this->docker->getContainerManager()->find($service->getContainer());

                $links[] = sprintf('%s:%s', $serviceContainer->getName(), $service->getName());
            }
        }

        $hostConfig->setLinks($links);

        $containerCreateResult = $this->docker->getContainerManager()->create($config);
        $attachStream = $this->docker->getContainerManager()->attach($containerCreateResult->getId(), [
            'stream' => true,
            'stdout' => true,
            'stderr' => true,
        ], ContainerManager::FETCH_STREAM);

        $attachStream->onStdout($this->logger->getRunStdoutCallback());
        $attachStream->onStderr($this->logger->getRunStderrCallback());

        $this->docker->getContainerManager()->start($containerCreateResult->getId());

        $attachStream->wait();

        $containerWait = $this->docker->getContainerManager()->wait($containerCreateResult->getId());

        return $containerWait->getStatusCode();
    }
}


<?php

namespace Joli\JoliCi\Command;

use Joli\JoliCi\Container;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class UpdateImageCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('images-update');
        $this->setDescription('Update docker images used to build test environnement');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $container = new Container();
        $docker = $container->getDocker();
        $logger = $container->getLoggerCallback((OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()));

        foreach ($docker->getImageManager()->findAll() as $image) {
            if (preg_match('#^jolicode/(.+?)$#', $image->getRepository())) {
                $output->writeln(sprintf("Update %s image", $image->getRepository()));
                $docker->getImageManager()->pull($image->getRepository(), 'latest', $logger->getBuildCallback());
            }
        }
    }
}


<?php

namespace Joli\JoliCi\BuildStrategy;

use Joli\JoliCi\Job;

class ChainBuildStrategy implements BuildStrategyInterface
{
    /**
     * @var BuildStrategyInterface[] A list of strategy to use for this builder
     */
    private $strategies = array();

    /**
     * Add a build strategy to builder
     *
     * @param BuildStrategyInterface $strategy Strategy to add
     */
    public function pushStrategy(BuildStrategyInterface $strategy)
    {
        $this->strategies[$strategy->getName()] = $strategy;
    }

    /**
     * {@inheritdoc}
     */
    public function getJobs($directory)
    {
        $builds = array();

        foreach ($this->strategies as $strategy) {
            if ($strategy->supportProject($directory)) {
                $builds += $strategy->getJobs($directory);
            }
        }

        return $builds;
    }

    /**
     * {@inheritdoc}
     */
    public function prepareJob(Job $job)
    {
        $this->strategies[$job->getStrategy()]->prepareJob($job);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'Chain';
    }

    /**
     * {@inheritdoc}
     */
    public function supportProject($directory)
    {
        foreach ($this->strategies as $strategy) {
            if ($strategy->supportProject($directory)) {
                return true;
            }
        }

        return false;
    }
}


<?php
/*
 * This file is part of JoliCi.
*
* (c) Joel Wurtz <jwurtz@jolicode.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliCi\Command;

use Joli\JoliCi\Container;
use Joli\JoliNotif\Notification;
use Joli\JoliNotif\NotifierFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class RunCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('run');
        $this->setDescription('Run tests on your project');
        $this->addOption('project-path', 'p', InputOption::VALUE_OPTIONAL, "Path where you project is (default to current directory)", ".");
        $this->addOption('keep', 'k', InputOption::VALUE_OPTIONAL, "Number of images / containers / directories per build to keep when cleaning at the end of run", 1);
        $this->addOption('no-cache', null, InputOption::VALUE_NONE, "Do not use cache of docker");
        $this->addOption('timeout', null, InputOption::VALUE_OPTIONAL, "Timeout for docker client in seconds (default to 5 minutes)", "300");
        $this->addOption('notify', null, InputOption::VALUE_NONE, "Show desktop notifications when a build is finished");
        $this->addArgument('cmd', InputArgument::OPTIONAL, "Override test command");
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $container  = new Container();
        $verbose    = (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity());
        $strategy   = $container->getChainStrategy();
        $executor   = $container->getExecutor(!$input->getOption('no-cache'), $verbose, $input->getOption('timeout'));
        $serviceManager = $container->getServiceManager($verbose);
        $notifier   = NotifierFactory::create();

        $output->writeln("<info>Creating builds...</info>");

        $jobs = $strategy->getJobs($input->getOption("project-path"));

        $output->writeln(sprintf("<info>%s builds created</info>", count($jobs)));

        $exitCode = 0;

        try {
            foreach ($jobs as $job) {
                $output->writeln(sprintf("\n<info>Running job %s</info>\n", $job->getDescription()));

                $serviceManager->start($job);
                $strategy->prepareJob($job);

                $success  = $executor->test($job, $input->getArgument('cmd'));
                $exitCode += $success == 0 ? 0 : 1;

                if ($input->getOption('notify')) {
                    $notification = new Notification();
                    $notification->setBody(sprintf('Test results for %s on %s', $container->getNaming()->getProjectName($input->getOption('project-path')), $job->getDescription()));
                    $notification->setTitle($success == 0 ? 'Tests passed' : 'Tests failed');

                    $notifier->send($notification);
                }

                $serviceManager->stop($job);
            }
        } catch (\Exception $e) {
            // Try stop last builds
            if (isset($job)) {
                $serviceManager->stop($job);
            }
            // We do not deal with exception (Console Component do it well),
            // we just catch it to allow cleaner to be runned even if one of the build failed hard
            // Simulation of a finally for php < 5.6 :-°
        }

        $container->getVacuum()->clean($input->getOption("project-path"), $input->getOption("keep"));

        if (isset($e)) {
            throw $e;
        }

        return $exitCode;
    }
}


<?php

namespace Joli\JoliCi;

use Docker\Docker;
use Joli\JoliCi\BuildStrategy\ChainBuildStrategy;
use Joli\JoliCi\Filesystem\Filesystem;
use Joli\JoliCi\Log\SimpleFormatter;
use Joli\JoliCi\BuildStrategy\TravisCiBuildStrategy;
use Joli\JoliCi\BuildStrategy\JoliCiBuildStrategy;
use Joli\JoliCi\Builder\DockerfileBuilder;
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Monolog\Handler\FingersCrossedHandler;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use TwigGenerator\Builder\Generator;

class Container
{
    private $docker;

    /**
     * Strategy based on the ".travis.yml" file
     *
     * @return TravisCiBuildStrategy
     */
    public function getTravisCiStrategy()
    {
        $builder   = new DockerfileBuilder();
        $generator = new Generator();
        $generator->setTemplateDirs(array(
            __DIR__."/../../../resources/templates",
        ));
        $generator->setMustOverwriteIfExists(true);
        $generator->addBuilder($builder);

        return new TravisCiBuildStrategy($builder, $this->getBuildPath(), $this->getNaming(), $this->getFilesystem());
    }

    /**
     * Strategy based on the ".jolici" folder
     *
     * @return JoliCiBuildStrategy
     */
    public function getJoliCiStrategy()
    {
        return new JoliCiBuildStrategy($this->getBuildPath(), $this->getNaming(), $this->getFilesystem());
    }

    /**
     * Chain strategy to allow multiples ones
     *
     * @return ChainBuildStrategy
     */
    public function getChainStrategy()
    {
        $strategy = new ChainBuildStrategy();
        $strategy->pushStrategy($this->getTravisCiStrategy());
        $strategy->pushStrategy($this->getJoliCiStrategy());

        return $strategy;
    }

    /**
     * Alias for the main strategy
     *
     * @return \Joli\JoliCi\BuildStrategy\BuildStrategyInterface
     */
    public function getStrategy()
    {
        return $this->getChainStrategy();
    }

    /**
     * Get a console with finger crossed handler
     *
     * @param bool $verbose
     *
     * @return Logger
     */
    public function getConsoleLogger($verbose = false)
    {
        $logger               = new Logger("standalone-logger");
        $handler              = new StreamHandler("php://stdout", $verbose ? Logger::DEBUG : Logger::INFO);
        $simpleFormatter      = new SimpleFormatter();

        $handler->setFormatter($simpleFormatter);
        $logger->pushHandler($handler);

        if (!$verbose) {
            $stdErrHandler = new StreamHandler("php://stderr", Logger::DEBUG);
            $fingerCrossedHandler = new FingersCrossedHandler($stdErrHandler, new ErrorLevelActivationStrategy(Logger::ERROR), 10);

            $logger->pushHandler($fingerCrossedHandler);
            $stdErrHandler->setFormatter($simpleFormatter);
        }

        return $logger;
    }

    public function getVacuum()
    {
        return new Vacuum($this->getDocker(), $this->getNaming(), $this->getStrategy(), $this->getFilesystem(), $this->getBuildPath());
    }

    public function getFilesystem()
    {
        return new Filesystem();
    }

    public function getDocker()
    {
        if (!$this->docker) {
            $this->docker = new Docker();
        }

        return $this->docker;
    }

    public function getExecutor($cache = true, $verbose = false, $timeout = 600)
    {
        return new Executor($this->getLoggerCallback($verbose), $this->getDocker(), $this->getBuildPath(), $cache, false, $timeout);
    }

    public function getServiceManager($verbose = false)
    {
        return new ServiceManager($this->getDocker(), $this->getLoggerCallback($verbose));
    }

    public function getBuildPath()
    {
        return sys_get_temp_dir().DIRECTORY_SEPARATOR.".jolici-builds";
    }

    public function getNaming()
    {
        return new Naming();
    }

    public function getLoggerCallback($verbose)
    {
        return new LoggerCallback($this->getConsoleLogger($verbose));
    }
}


<?php

namespace Baka\Mail;

use Exception;

/**
 * Class Message
 *
 * @package Phalcon\Mailer
 */
class Message extends \Phalcon\Mailer\Message
{
    protected $queueName = 'email_queue';
    protected $viewPath = null;
    protected $params = null;
    protected $viewsDirLocal = null;
    protected $smtp = null;
    protected $auth = false;

    /**
     * Set the body of this message, either as a string, or as an instance of
     * {@link \Swift_OutputByteStream}.
     *
     * @param mixed $content
     * @param string $contentType optional
     * @param string $charset     optional
     *
     * @return $this
     *
     * @see \Swift_Message::setBody()
     */
    public function content($content, $contentType = self::CONTENT_TYPE_HTML, $charset = null)
    {
        if (isset($this->params) && is_array($this->params)) {
            $content = $this->setDynamicContent($this->params, $content);
        }

        $this->getMessage()->setBody($content, $contentType, $charset);

        return $this;
    }

    /**
     * Send the given Message like it would be sent in a mail client.
     *
     * All recipients (with the exception of Bcc) will be able to see the other
     * recipients this message was sent to.
     *
     * Recipient/sender data will be retrieved from the Message object.
     *
     * The return value is the number of recipients who were accepted for
     * delivery.
     *
     * Events:
     * - mailer:beforeSend
     * - mailer:afterSend
     *
     * @return int
     *
     * @see \Swift_Mailer::send()
     */
    public function send()
    {
        $eventManager = $this->getManager()->getEventsManager();
        if ($eventManager) {
            $result = $eventManager->fire('mailer:beforeSend', $this);
        } else {
            $result = true;
        }

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

        $this->failedRecipients = [];

        //send to queue
        $queue = $this->getManager()->getQueue();
        //$queueName = $this->

        if ($this->auth) {
            $queue->putInTube($this->queueName, [
                'message' => $this->getMessage(),
                'auth' => $this->smtp,
            ]);
        } else {
            $queue->putInTube($this->queueName, $this->getMessage());
        }

        /* $count = $this->getManager()->getSwift()->send($this->getMessage(), $this->failedRecipients);
    if ($eventManager) {
    $eventManager->fire('mailer:afterSend', $this, [$count, $this->failedRecipients]);
    }
    return $count;*/
    }

    /**
     * Send message instantly, without a queue
     *
     * @return void
     */
    public function sendNow()
    {
        $config = $this->getManager()->getDI()->getConfig();
        $message = $this->getMessage();

        $username = $config->email->username;
        $password = $config->email->password;
        $host = $config->email->host;
        $port = $config->email->port;

        $transport = \Swift_SmtpTransport::newInstance($host, $port);

        $transport->setUsername($username);
        $transport->setPassword($password);

        $swift = \Swift_Mailer::newInstance($transport);

        $failures = [];

        $swift->send($message, $failures);
    }

    /**
     * Overwrite the baka SMTP connection for this current email
     *
     * @param  array  $smtp
     * @return this
     */
    public function smtp(array $params)
    {
        //validate the user params
        if (!array_key_exists('username', $params)) {
            throw new Exception('We need a username');
        }

        if (!array_key_exists('password', $params)) {
            throw new Exception('We need a password');
        }

        $this->smtp = $params;
        $this->auth = true;

        return $this;
    }

    /**
     * Set the queue name if the user wants to shange it
     *
     * @param string $queuName
     *
     * @return $this
     */
    public function queue($queue)
    {
        $this->queueName = $queue;
        return $this;
    }

    /**
     * Set variables to views
     *
     * @param string $params
     *
     * @return $this
     */
    public function params($params)
    {
        $this->params = $params;
        return $this;
    }

    /**
     * The local path to the folder viewsDir only this message. (OPTIONAL)
     *
     * @param string $dir
     *
     * @return $this
     */
    public function viewDir($dir)
    {
        $this->viewsDirLocal = $dir;
        return $this;
    }

    /**
     * view relative to the folder viewsDir (REQUIRED)
     *
     * @param string $template
     *
     * @return $this
     */
    public function template($template = 'email.volt')
    {
        $this->viewPath = $template;

        //if we have params thats means we are using a template
        if (is_array($this->params)) {
            $content = $this->getManager()->setRenderView($this->viewPath, $this->params);
        }

        $this->getMessage()->setBody($content, self::CONTENT_TYPE_HTML);

        return $this;
    }

    /**
     * Set content dynamically by params
     * @param $params
     * @param $content
     * @return string
     */
    public function setDynamicContent(array $params, string $content)
    {
        $processed_content = preg_replace_callback(
            '~\{(.*?)\}~si',
            function ($match) use ($params) {
                return str_replace($match[0], isset($params[$match[1]]) ? $params[$match[1]] : $match[0], $match[0]);
            },
            $content
        );

        return $processed_content;
    }
}


<?php

namespace Baka\Mail;

use Phalcon\Queue\Beanstalk\Extended as BeanstalkExtended;
use Phalcon\Queue\Beanstalk\Job;
use Swift_Mime_Message;
use Throwable;

trait JobTrait
{
    /**
     * @description("Email queue")
     *
     * @param({'type'='string', 'name'='queueName', 'description'='name of the queue , default email_queue' })
     *
     * @return void
     */
    public function mailQueueAction($queueName)
    {
        $this->validateConfigurations();

        //call queue
        $queue = new BeanstalkExtended([
            'host' => $this->config->beanstalk->host,
            'prefix' => $this->config->beanstalk->prefix,
        ]);

        //dependent variables
        $config = $this->config;
        $di = \Phalcon\DI\FactoryDefault::getDefault();

        //call queue tube
        $queue->addWorker($queueName[0], function (Job $job) use ($di, $config) {
            try {
                $jobBody = $job->getBody();
                $auth = null;

                //if its a array then we know we are getting more settings
                if (is_array($jobBody) && array_key_exists('auth', $jobBody) && array_key_exists('message', $jobBody)) {
                    $message = $jobBody['message'];
                    $auth = $jobBody['auth'];
                } else {
                    //its just normal email
                    $message = $jobBody;
                }

                //vaalidate
                if (!$message instanceof Swift_Mime_Message) {
                    $this->log->error('Something went wrong with the message we are trying to send ', [$message]);
                    return;
                }

                //email auth settings
                $username = $config->email->username;
                $password = $config->email->password;
                $host = $config->email->host;
                $port = $config->email->port;

                //if get the the auth we need ot overwrite it
                if ($auth) {
                    $username = $auth['username'];
                    $password = $auth['password'];

                    //ovewrite host
                    if (array_key_exists('host', $auth)) {
                        $host = $auth['host'];
                    }

                    //ovewrite port
                    if (array_key_exists('port', $auth)) {
                        $port = $auth['port'];
                    }
                }

                //email configuration
                $transport = \Swift_SmtpTransport::newInstance($host, $port);

                $transport->setUsername($username);
                $transport->setPassword($password);

                $swift = \Swift_Mailer::newInstance($transport);

                $failures = [];
                if ($recipients = $swift->send($message, $failures)) {
                    $this->log->info('EmailTask Message successfully sent to:', [$message->getTo()]);
                } else {
                    $this->log->error('EmailTask There was an error: ', [$failures]);
                }
            } catch (Throwable $e) {
                $this->log->error($e->getMessage());
                echo $e->getMessage() . "\n";
            }

            // It's very important to send the right exit code!
            exit(0);
        });

        // Start processing queues
        $queue->doWork();
    }

    /**
     * Runs all the configuration validations needed
     *
     * @return void
     */
    protected function validateConfigurations()
    {
        //$this->validateQueueNameConfiguration();
        $this->validateBeanstalkConfiguration();
        $this->validateEmailConfiguration();
    }

    /**
     * Validates whether a queue name was provided or not
     *
     * @return void
     */
    protected function validateQueueNameConfiguration()
    {
        if (empty($queueName)) {
            echo "\nYou have to define a queue name.\n\n";
            return;
        }
    }

    /**
     * Validates whether beanstlak is configured or not
     *
     * @return void
     */
    protected function validateBeanstalkConfiguration()
    {
        if (!is_object($this->config->beanstalk)) {
            echo "\nNeed to configure beanstalkd on your phalcon configuration.\n\n";
            return;
        }
    }

    /**
     * Validates whether email is configured or not
     *
     * @return void
     */
    protected function validateEmailConfiguration()
    {
        if (!is_object($this->config->email)) {
            echo "\nNeed to configure email on your phalcon configuration.\n\n";
            return;
        }
    }
}


<?php

namespace Baka\Mail;

use Phalcon\Mvc\View\Engine\Volt;

/**
 * Class Manager
 *
 * @package Phalcon\Manager
 */
class Manager extends \Phalcon\Mailer\Manager
{
    protected $queue;

    /**
     *  Overwrite this funciton to use ower mail message
     *
     *  Create a new Message instance.
     *
     * Events:
     * - mailer:beforeCreateMessage
     * - mailer:afterCreateMessage
     *
     * @return \Phalcon\Mailer\Message
     */
    public function createMessage()
    {
        $eventsManager = $this->getEventsManager();
        if ($eventsManager) {
            $eventsManager->fire('mailer:beforeCreateMessage', $this);
        }

        /** @var $message Message */
        $message = $this->getDI()->get('\Baka\Mail\Message', [$this]);
        if (($from = $this->getConfig('from'))) {
            $message->from($from['email'], isset($from['name']) ? $from['name'] : null);
        }

        if ($eventsManager) {
            $eventsManager->fire('mailer:afterCreateMessage', $this, $message);
        }
        return $message;
    }

    /**
     * Configure MailerManager class
     *
     * @param array $config
     *
     * @see \Phalcon\Mailer\Manager::registerSwiftTransport()
     * @see \Phalcon\Mailer\Manager::registerSwiftMailer()
     */
    protected function configure(array $config)
    {
        $this->config = $config;

        $this->registerSwiftTransport();
        $this->registerSwiftMailer();
        $this->registerQueue();
    }

    /**
     * Register the queue service
     *
     * @return BeanstalkExtended
     */
    public function registerQueue()
    {
        $this->queue = $this->getDI()->get('queue');
    }

    /**
     * Get the queue service
     *
     * @return BeanstalkExtended
     */
    public function getQueue()
    {
        return $this->queue;
    }

    /**
     * Renders a view
     *
     * @param $viewPath
     * @param $params
     * @param null $viewsDir
     *
     * @return string
     */
    public function setRenderView($viewPath, $params)
    {
        //Set volt tempalte enging and specify the cache path
        $this->setViewEngines([
            '.volt' => function ($view = null) {
                $volt = new Volt($view);

                $volt->setOptions([
                    'compiledPath' => APP_PATH . '/cache/volt/',
                    'compiledSeparator' => '_',
                    'compileAlways' => !$this->getDI()->get('config')->application->production,
                ]);

                return $volt;
            }
        ]);

        $view = $this->getView();

        $content = $view->render($viewPath, $params);
        return $content;
    }
}


<?php namespace DCarbone\PHPFHIR\ClassGenerator;

/*
 * Copyright 2016-2018 Daniel Carbone (daniel.p.carbone@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use DCarbone\PHPFHIR\ClassGenerator\Generator\ClassGenerator;
use DCarbone\PHPFHIR\ClassGenerator\Generator\MethodGenerator;
use DCarbone\PHPFHIR\ClassGenerator\Generator\XSDMapGenerator;
use DCarbone\PHPFHIR\ClassGenerator\Template\PHPFHIR\AutoloaderTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Template\PHPFHIR\HelperTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Template\PHPFHIR\ParserMapTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Template\PHPFHIR\ResponseParserTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Utilities\CopyrightUtils;
use DCarbone\PHPFHIR\ClassGenerator\Utilities\FileUtils;

/**
 * Class Generator
 * @package PHPFHIR
 */
class Generator
{

    /** @var \DCarbone\PHPFHIR\ClassGenerator\Config */
    protected $config;

    /** @var XSDMap */
    protected $XSDMap;

    /** @var AutoloaderTemplate */
    private $autoloadMap;
    /** @var ParserMapTemplate */
    private $mapTemplate;

    /**
     * Generator constructor.
     * @param \DCarbone\PHPFHIR\ClassGenerator\Config $config
     */
    public function __construct(Config $config)
    {
        $this->config = $config;
    }

    /**
     * Generate FHIR object classes based on XSD
     */
    public function generate()
    {
        $this->beforeGeneration();

        $this->config->getLogger()->startBreak('Class Generation');
        foreach ($this->XSDMap as $fhirElementName => $mapEntry) {
            $this->config->getLogger()->debug("Generating class for element {$fhirElementName}...");
            $classTemplate = ClassGenerator::buildFHIRElementClassTemplate($this->config, $this->XSDMap, $mapEntry);

            FileUtils::createDirsFromNS($classTemplate->getNamespace(), $this->config);

            // Generate class file
            MethodGenerator::implementConstructor($this->config, $classTemplate);
            $classTemplate->writeToFile($this->config->getOutputPath());

            $this->mapTemplate->addEntry($classTemplate);
            $this->autoloadMap->addPHPFHIRClassEntry($classTemplate);
            $this->config->getLogger()->debug("{$fhirElementName} completed.");
        }
        $this->config->getLogger()->endBreak('Class Generation');

        $this->afterGeneration();
    }

    /**
     * Commands to run prior to class generation
     */
    protected function beforeGeneration()
    {
        // Class props
        $this->config->getLogger()->startBreak('XSD Parsing');
        $this->XSDMap = XSDMapGenerator::buildXSDMap($this->config);
        $this->config->getLogger()->endBreak('XSD Parsing');

        // Initialize some classes and things.
        $this->config->getLogger()->startBreak('Generator Class Initialization');
        self::_initializeClasses($this->config);

        $this->autoloadMap = new AutoloaderTemplate($this->config);

        $this->mapTemplate = new ParserMapTemplate($this->config);
        $this->autoloadMap->addEntry(
            $this->mapTemplate->getClassName(),
            $this->mapTemplate->getClassPath()
        );

        $helperTemplate = new HelperTemplate($this->config);
        $helperTemplate->writeToFile();
        $this->autoloadMap->addEntry(
            $helperTemplate->getClassName(),
            $helperTemplate->getClassPath()
        );

        $this->config->getLogger()->endBreak('Generator Class Initialization');
    }

    /**
     * @param \DCarbone\PHPFHIR\ClassGenerator\Config $config
     */
    private static function _initializeClasses(Config $config)
    {
        $config->getLogger()->info('Compiling Copyrights...');
        CopyrightUtils::compileCopyrights($config);

        $config->getLogger()->info('Creating root directory...');
        FileUtils::createDirsFromNS($config->getOutputNamespace(), $config);
    }

    /**
     * Commands to run after class generation
     */
    protected function afterGeneration()
    {
        $this->mapTemplate->writeToFile();
        $this->autoloadMap->writeToFile();

        $responseParserTemplate = new ResponseParserTemplate($this->config);
        $this->autoloadMap->addEntry(
            $responseParserTemplate->getClassName(),
            $responseParserTemplate->getClassPath()
        );
        $responseParserTemplate->writeToFile();
    }
}


<?php namespace DCarbone\PHPFHIR\ClassGenerator\Template\Method;

/*
 * Copyright 2016-2018 Daniel Carbone (daniel.p.carbone@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use DCarbone\PHPFHIR\ClassGenerator\Config;
use DCarbone\PHPFHIR\ClassGenerator\Enum\PHPScopeEnum;
use DCarbone\PHPFHIR\ClassGenerator\Template\AbstractTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Template\Parameter\BaseParameterTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Utilities\NameUtils;

/**
 * Class BaseMethodTemplate
 * @package DCarbone\PHPFHIR\ClassGenerator\Template
 */
class BaseMethodTemplate extends AbstractTemplate
{
    /** @var \DCarbone\PHPFHIR\ClassGenerator\Config */
    protected $config;

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

    /** @var \DCarbone\PHPFHIR\ClassGenerator\Enum\PHPScopeEnum|null */
    protected $scope;

    /** @var \DCarbone\PHPFHIR\ClassGenerator\Template\Parameter\BaseParameterTemplate[] */
    protected $parameters = [];

    /** @var null|string */
    protected $returnValueType = null;
    /** @var null|string */
    protected $returnStatement = null;

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

    /**
     * BaseMethodTemplate constructor.
     * @param \DCarbone\PHPFHIR\ClassGenerator\Config $config
     * @param string $name
     * @param \DCarbone\PHPFHIR\ClassGenerator\Enum\PHPScopeEnum|null $scope
     */
    public function __construct(Config $config, $name, PHPScopeEnum $scope = null)
    {
        $this->config = $config;

        if (NameUtils::isValidFunctionName($name)) {
            $this->name = $name;
        } else {
            throw new \InvalidArgumentException('Function name "' . $name . '" is not valid.');
        }

        if (null === $scope) {
            $this->scope = new PHPScopeEnum(PHPScopeEnum::_PUBLIC);
        } else {
            $this->scope = $scope;
        }
    }

    /**
     * @param string $line
     */
    public function addLineToBody($line)
    {
        $this->body[] = $line;
    }

    /**
     * Add an entire block of code to the body of this method
     *
     * @param string $block
     */
    public function addBlockToBody($block)
    {
        $this->body = array_merge($this->body, explode(PHP_EOL, $block));
    }

    /**
     * @return null|string
     */
    public function getReturnValueType()
    {
        return $this->returnValueType;
    }

    /**
     * @param null|string $returnValueType
     */
    public function setReturnValueType($returnValueType)
    {
        $this->returnValueType = $returnValueType;
    }

    /**
     * @return null|string
     */
    public function getReturnStatement()
    {
        return $this->returnStatement;
    }

    /**
     * @param null|string $returnStatement
     */
    public function setReturnStatement($returnStatement)
    {
        $this->returnStatement = $returnStatement;
    }

    /**
     * @param BaseParameterTemplate $parameterTemplate
     */
    public function addParameter(BaseParameterTemplate $parameterTemplate)
    {
        $this->parameters[$parameterTemplate->getName()] = $parameterTemplate;
    }

    /**
     * @param string $name
     * @return bool
     */
    public function hasParameter($name)
    {
        return isset($this->parameters[$name]);
    }

    /**
     * @param string $name
     * @return BaseParameterTemplate
     */
    public function getParameter($name)
    {
        return $this->parameters[$name];
    }

    /**
     * @return string
     */
    public function compileTemplate()
    {
        return sprintf('%s%s', $this->buildDocBlock(), $this->buildMethodDefinition());
    }

    /**
     * @return string
     */
    protected function buildDocBlock()
    {
        $output = sprintf("    /**\n%s", $this->getDocBlockDocumentationFragment());

        foreach ($this->getParameters() as $param) {
            $output = sprintf(
                "%s     * %s\n",
                $output,
                $param->getParamDocBlockFragment()
            );
        }

        return sprintf("%s%s     */\n", $output, $this->buildReturnDocBlockStatement());
    }

    /**
     * @return BaseParameterTemplate[]
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * @return string
     */
    protected function buildReturnDocBlockStatement()
    {
        if (is_string($this->returnValueType)) {
            return sprintf("     * @return %s\n", $this->returnValueType);
        }

        return '';
    }

    /**
     * @return string
     */
    protected function buildMethodDefinition()
    {
        return sprintf(
            "    %s function %s(%s) {\n%s    }\n\n",
            (string)$this->getScope(),
            $this->getName(),
            $this->buildMethodParameterDefinition(),
            $this->buildMethodBody()
        );
    }

    /**
     * @return PHPScopeEnum
     */
    public function getScope()
    {
        return $this->scope;
    }

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

    /**
     * @return string
     */
    protected function buildMethodParameterDefinition()
    {
        $output = '';
        $params = [];
        foreach ($this->getParameters() as $param) {
            $params[] = $param->compileTemplate();
        }

        return sprintf('%s%s', $output, implode(', ', $params));
    }

    /**
     * @return string
     */
    protected function buildMethodBody()
    {
        $output = '';
        foreach ($this->getBody() as $line) {
            $output = sprintf("%s        %s\n", $output, $line);
        }

        return sprintf('%s%s', $output, $this->buildMethodReturnStatement());
    }

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

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

    /**
     * @return string
     */
    protected function buildMethodReturnStatement()
    {
        if (is_string($this->returnStatement)) {
            return sprintf("        return %s;\n", $this->returnStatement);
        }

        return '';
    }
}


<?php namespace DCarbone\PHPFHIR\ClassGenerator\Generator;

/*
 * Copyright 2016-2018 Daniel Carbone (daniel.p.carbone@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use DCarbone\PHPFHIR\ClassGenerator\Enum\ElementTypeEnum;
use DCarbone\PHPFHIR\ClassGenerator\Template\ClassTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Template\Property\BasePropertyTemplate;
use DCarbone\PHPFHIR\ClassGenerator\Utilities\XMLUtils;
use DCarbone\PHPFHIR\ClassGenerator\XSDMap;

/**
 * Class PropertyGenerator
 * @package DCarbone\PHPFHIR\ClassGenerator\Utilities
 */
abstract class PropertyGenerator
{
    /**
     * TODO: I don't like how this is utilized, really.  Should think of a better way to do it.
     *
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $propertyElement
     */
    public static function implementProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $propertyElement
    ) {
        switch (strtolower($propertyElement->getName())) {
            case ElementTypeEnum::ATTRIBUTE:
                self::implementAttributeProperty($XSDMap, $classTemplate, $propertyElement);
                break;
            case ElementTypeEnum::CHOICE:
                self::implementChoiceProperty($XSDMap, $classTemplate, $propertyElement);
                break;
            case ElementTypeEnum::SEQUENCE:
                self::implementPropertySequence($XSDMap, $classTemplate, $propertyElement);
                break;
            case ElementTypeEnum::UNION:
                self::implementUnionProperty($XSDMap, $classTemplate, $propertyElement);
                break;
            case ElementTypeEnum::ENUMERATION:
                self::implementEnumerationProperty($XSDMap, $classTemplate, $propertyElement);
                break;
        }
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $attribute
     */
    public static function implementAttributeProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $attribute
    ) {
        $propertyTemplate = self::buildProperty($XSDMap, $classTemplate, $attribute);
        if ($propertyTemplate &&
            false ===
            static::isPropertyImplementedByParent($XSDMap, $classTemplate->getXSDMapEntry(), $propertyTemplate)) {
            $classTemplate->addProperty($propertyTemplate);
        }
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $element
     * @param string $documentation
     * @param string $maxOccurs
     * @return BasePropertyTemplate|null
     */
    public static function buildProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $element,
        $documentation = null,
        $maxOccurs = null
    ) {
        $propertyTemplate = new BasePropertyTemplate();

        $attributes = $element->attributes();

        if (null === $documentation) {
            $propertyTemplate->setDocumentation(XMLUtils::getDocumentation($element));
        }

        if (null === $maxOccurs && isset($attributes['maxOccurs'])) {
            $maxOccurs = (string)$attributes['maxOccurs'];
        }

        if (null !== $maxOccurs && '' !== $maxOccurs) {
            $propertyTemplate->setCollection(self::determineIfCollection($maxOccurs));
        }

        $name = (string)$attributes['name'];
        $type = (string)$attributes['type'];
        $ref = (string)$attributes['ref'];

        if ('' === $name) {
            if ('' === $ref) {
                trigger_error(sprintf(
                    'Encountered property on FHIR object "%s" with no "name" or "ref" attribute, cannot create property for it.  Property definition: "%s"',
                    $classTemplate->getElementName(),
                    $element->saveXML()
                ));

                return null;
            }

            if (0 === strpos($ref, 'xhtml')) {
                $propertyTemplate->setName(substr($ref, 6));
                $propertyTemplate->setFHIRElementType('html');
                $propertyTemplate->setHTML(true);
                $propertyTemplate->setPHPType('string');

                return $propertyTemplate;
            }

            $name = $ref;
            $type = $ref;
        }

        $propertyTemplate->setName($name);
        $propertyTemplate->setFHIRElementType($type);

        // TODO: Implement proper primitive types
        if (false !== strpos($type, '-primitive')) {
            $propertyTemplate->setPrimitive(true);
            $propertyTemplate->setPHPType('string');
        } else {
            if (false !== strpos($type, '-list')) {
                $propertyTemplate->setList(true);
                $propertyTemplate->setPHPType('string');
            } else {
                if (0 === strpos($type, 'xs:')) {
                    $propertyTemplate->setPHPType(
                        $XSDMap->getClassUseStatementForFHIRElementName(substr($type, 3))
                    );
                } else {
                    $propertyTemplate->setPHPType(
                        $XSDMap->getClassUseStatementForFHIRElementName($type)
                    );
                }
            }
        }

        return $propertyTemplate;
    }

    /**
     * @param string|number $maxOccurs
     * @return bool
     */
    public static function determineIfCollection($maxOccurs)
    {
        return 'unbounded' === strtolower($maxOccurs) || (is_numeric($maxOccurs) && (int)$maxOccurs > 1);
    }

    /**
     * @param XSDMap $XSDMap
     * @param XSDMap\XSDMapEntry $XSDMapEntry
     * @param BasePropertyTemplate $propertyTemplate
     * @return bool
     */
    public static function isPropertyImplementedByParent(
        XSDMap $XSDMap,
        XSDMap\XSDMapEntry $XSDMapEntry,
        BasePropertyTemplate $propertyTemplate
    ) {
        $parentMapEntry = $XSDMapEntry->getExtendedMapEntry();
        if (null === $parentMapEntry) {
            return false;
        }

        $propertyName = $propertyTemplate->getName();
        $propertyType = $propertyTemplate->getFHIRElementType();

        foreach ($parentMapEntry->getProperties() as $name => $type) {
            if ($propertyName === $name && $propertyType === $type) {
                return true;
            }
        }

        return static::isPropertyImplementedByParent($XSDMap, $parentMapEntry, $propertyTemplate);
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $choice
     */
    public static function implementChoiceProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $choice
    ) {
        $attributes = $choice->attributes();
//        $minOccurs = (int)$attributes['minOccurs'];
        $maxOccurs = $attributes['maxOccurs'];
        $documentation = XMLUtils::getDocumentation($choice);

        foreach ($choice->xpath('xs:element') as $element) {
            $propertyTemplate = self::buildProperty($XSDMap, $classTemplate, $element, $documentation, $maxOccurs);
            if ($propertyTemplate &&
                false ===
                static::isPropertyImplementedByParent($XSDMap, $classTemplate->getXSDMapEntry(), $propertyTemplate)) {
                $classTemplate->addProperty($propertyTemplate);
            }
        }
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $sequence
     */
    public static function implementPropertySequence(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $sequence
    ) {
        foreach ($sequence->children('xs', true) as $element) {
            /** @var \SimpleXMLElement $element */
            switch (strtolower($element->getName())) {
                case ElementTypeEnum::ELEMENT:
                    self::implementElementProperty($XSDMap, $classTemplate, $element);
                    break;

                case ElementTypeEnum::CHOICE:
                    self::implementChoiceProperty($XSDMap, $classTemplate, $element);
                    break;
            }
        }
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $element
     */
    public static function implementElementProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $element
    ) {
        $propertyTemplate = self::buildProperty($XSDMap, $classTemplate, $element);
        if ($propertyTemplate &&
            false ===
            static::isPropertyImplementedByParent($XSDMap, $classTemplate->getXSDMapEntry(), $propertyTemplate)) {
            $classTemplate->addProperty($propertyTemplate);
        }
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $union
     */
    public static function implementUnionProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $union
    ) {
        // TODO: Implement these!
    }

    /**
     * @param XSDMap $XSDMap
     * @param ClassTemplate $classTemplate
     * @param \SimpleXMLElement $enumeration
     */
    public static function implementEnumerationProperty(
        XSDMap $XSDMap,
        ClassTemplate $classTemplate,
        \SimpleXMLElement $enumeration
    ) {
        // TODO: Implement these!
    }
}


<?php

namespace Anax\View;

use Anax\Commons\ContainerInjectableInterface;
use Anax\Commons\ContainerInjectableTrait;

/**
 * A view collection supporting Anax DI, store all views per region,
 * render at will.
 */
class ViewCollection implements
    ContainerInjectableInterface
{
    use ContainerInjectableTrait;

    /**
     * @var array $views container for all views.
     */
    private $views = [];

    /**
     * @var array  $paths  where to look for template files.
     * @var string $suffix add to each template file name.
     */
    private $paths = [];
    private $suffix = ".php";

    /**
     * Set paths to search through when looking for template files.
     *
     * @param array $paths with directories to search through.
     *
     * @return self
     */
    public function setPaths(array $paths) : object
    {
        foreach ($paths as $path) {
            if (!(is_dir($path) && is_readable($path))) {
                throw new Exception("Directory '$path' is not readable.");
            }
        }
        $this->paths = $paths;
        return $this;
    }

    /**
     * Set suffix to add last to template file givven, as a filename extension.
     *
     * @param string $suffix to use as file extension.
     *
     * @return self
     */
    public function setSuffix(string $suffix) : object
    {
        $this->suffix = $suffix;
        return $this;
    }

    /**
     * Convert template to path to template file and check that it exists.
     *
     * @param string $template the name of the template file to include
     *
     * @throws Anax\View\Exception when template file is missing
     *
     * @return string as path to the template file
     */
    public function getTemplateFile($template)
    {
        $file = $template . $this->suffix;
        if (is_file($file)) {
            return $file;
        }

        foreach ($this->paths as $path) {
            $file = $path . "/" . $template . $this->suffix;
            if (is_file($file)) {
                return $file;
            }
        }

        throw new Exception("Could not find template file '$template'.");
    }

    /**
     * Add (create) a view to be included, pass optional data and put the
     * view in an optional specific region (default region is "main") and
     * pass an optional sort value where the highest value is rendered first.
     * The $template can be a:
     *  filename (string),
     *  callback (array with key callback set to a callable array),
     *  view array (key value array with template, data, region, sort)
     *
     * @param array|string  $template the name of the template file to include.
     * @param array         $data     variables to make available to the view,
     *                                default is empty.
     * @param string        $region   which region to attach the view, default
     *                                is "main".
     * @param integer       $sort     which order to display the views.
     *
     * @return self for chaining.
     */
    public function add(
        $template,
        array $data = [],
        string $region = "main",
        int $sort = 0
    ) : object {
        $view = new View();

        if (empty($template)) {
            $tpl = null;
            $type = "empty";
        } elseif (is_string($template)) {
            $tpl = $this->getTemplateFile($template);
            $type = "file";
        } elseif (is_array($template)) {
            // Can be array with complete view or array with callable callback
            $tpl = $template;
            $type = "empty";
            $region = $tpl["region"] ?? $region;

            if (isset($tpl["callback"])) {
                $tpl["template"] = $template;
                $tpl["type"] = "callback";
            } elseif (isset($tpl["template"])) {
                if (!isset($tpl["type"]) || $tpl["type"] === "file") {
                    $tpl["type"] = "file";
                    $tpl["template"] = $this->getTemplateFile($tpl["template"]);
                }
            }
        }

        $view->set($tpl, $data, $sort, $type);
        $this->views[$region][] = $view;

        return $this;
    }

    /**
     * Add a callback to be rendered as a view.
     *
     * @param string $callback function to call to get the content of the view
     * @param array  $data     variables to make available to the view, default is empty
     * @param string $region   which region to attach the view
     * @param int    $sort     which order to display the views
     *
     * @return $this
     */
    public function addCallback($callback, $data = [], $region = "main", $sort = 0)
    {
        $view = new View();
        $view->set(["callback" => $callback], $data, $sort, "callback");
        $this->views[$region][] = $view;

        return $this;
    }

    /**
     * Add a string as a view.
     *
     * @param string $content the content
     * @param string $region  which region to attach the view
     * @param int    $sort    which order to display the views
     *
     * @return $this
     */
    public function addString($content, $region = "main", $sort = 0)
    {
        $view = new View();
        $view->set($content, [], $sort, "string");
        $this->views[$region][] = $view;

        return $this;
    }

    /**
     * Check if a region has views to render.
     *
     * @param string $region which region to check
     *
     * @return $this
     */
    public function hasContent($region)
    {
        return isset($this->views[$region]);
    }

    /**
     * Render all views for a specific region.
     *
     * @param string $region which region to use
     *
     * @return void
     */
    public function render($region = "main")
    {
        if (!isset($this->views[$region])) {
            return $this;
        }

        mergesort($this->views[$region], function ($viewA, $viewB) {
            $sortA = $viewA->sortOrder();
            $sortB = $viewB->sortOrder();

            if ($sortA == $sortB) {
                return 0;
            }

            return $sortA < $sortB ? -1 : 1;
        });

        foreach ($this->views[$region] as $view) {
            $view->render($this->di);
        }
    }

    /**
     * Render all views for a specific region and buffer the result.
     *
     * @param string $region which region to use.
     *
     * @return string with the buffered results.
     */
    public function renderBuffered($region = "main")
    {
        ob_start();
        $this->render($region);
        $res = ob_get_contents();
        ob_end_clean();
        return $res;
    }
}


<?php

namespace Anax\View;

use Anax\Commons\ContainerInjectableInterface;
use Anax\Commons\ContainerInjectableTrait;

/**
 * Render a view based on a template file and a dataset.
 */
class ViewRenderFile implements
    ViewRenderFileInterface,
    ContainerInjectableInterface
{
    use ContainerInjectableTrait;

    /**
     * Render the view file.
     *
     * @param string $file to include as view file.
     * @param array  $data to expose within the view.
     *
     * @throws \Anax\View\Exception when template file is not found.
     *
     * @return void
     */
    public function render(string $file, array $data) : void
    {
        if (!is_readable($file)) {
            throw new Exception("Could not find template file: " . $this->template);
        }

        $di = $this->di;
        $app = null;
        if ($di->has("app")) {
            $app = $di->get("app");
        }
        extract($data);
        require $file;
    }
}


<?php

namespace Anax\View;

use Anax\View\Exception;
use Anax\View\ViewRenderFile;
use Psr\Container\ContainerInterface;

/**
 * A view connected to a template file, supporting Anax DI.
 */
class View
{
    /**
     * @var $template     Template file or array
     * @var $templateData Data to send to template file
     * @var $sortOrder    For sorting views
     * @var $type         Type of view
     */
    private $template;
    private $templateData = [];
    private $sortOrder;
    private $type;

    /**
     * Set values for the view.
     *
     * @param array|string|callable $template the template file, array
     *                                        or callable
     * @param array        $data     variables to make available to the
     *                               view, default is empty
     * @param integer      $sort     which order to display the views,
     *                               if suitable
     * @param string       $type     which type of view
     *
     * @return self
     */
    public function set($template, $data = [], $sort = 0, $type = "file")
    {
        if (empty($template)) {
            $type = "empty";
        } elseif (is_array($template)) {
            if (isset($template["callback"])) {
                $type = "callback";
                $this->template = $template["callback"];
            } else {
                $this->template = $template["template"];
            }

            $this->sortOrder = $template["sort"] ?? $sort;
            $this->type = $template["type"] ?? $type;

            // Merge data array
            $data1 = $template["data"] ?? [];
            if (empty($data)) {
                $this->templateData = $data1;
            } else if (empty($data1)) {
                $this->templateData = $data;
            } else {
                foreach ($data as $key => $val) {
                    if (is_array($val)) {
                        if (!array_key_exists($key, $data1)) {
                            $data1[$key] = [];
                        }
                        $data1[$key] = array_merge($data1[$key], $val);
                    } else {
                        $data1[$key] = $val;
                    }
                    $this->templateData = $data1;
                }
            }

            return;
        }

        $this->template     = $template;
        $this->templateData = $data;
        $this->sortOrder    = $sort;
        $this->type         = $type;

        return $this;
    }

    /**
     * Render the view by its type.
     *
     * @param object $di optional with access to the framework resources.
     *
     * @return void
     */
    public function render(ContainerInterface $di = null)
    {
        switch ($this->type) {
            case "file":
                if ($di->has("viewRenderFile")) {
                    $viewRender = $di->get("viewRenderFile");
                } else {
                    $viewRender = new ViewRenderFile($di);
                    $viewRender->setDI($di);
                }
                $viewRender->render($this->template, $this->templateData);
                break;

            case "callback":
                if (!is_callable($this->template)) {
                    throw new Exception("View is expecting a valid callback, provided callback seems to not be a callable.");
                }
                echo call_user_func($this->template, $this->templateData);
                break;

            case "string":
                echo $this->template;
                break;

            case "empty":
                break;

            default:
                throw new Exception("Not a valid template type: '{$this->type}'.");
        }
    }

    /**
     * Give the sort order for this view.
     *
     * @return int
     */
    public function sortOrder()
    {
        return $this->sortOrder;
    }
}


<?php
declare(strict_types = 1);

namespace Ayesh\PHP_Timer;

/**
 * Class Timer
 *
 * Helper class to measure the execution time between two points in a single
 * request.
 *
 * @package Ayesh\PHP_Timer
 */
class Timer {
  public const FORMAT_PRECISE = FALSE;
  public const FORMAT_MILLISECONDS = 'ms';
  public const FORMAT_SECONDS = 's';
  public const FORMAT_HUMAN = 'h';

  /**
   * Stores all the timers statically.
   * @var Stopwatch[]
   */
  static private $timers = [];

  /**
   * Start or resume the timer.
   *
   * Call this method to start the timer with a given key. The default key
   * is "default", and used in @see \Ayesh\PHP_Timer\Timer::read() and reset()
   * methods as well
   *
   * Calling this with the same $key will not restart the timer if it has already
   * started.
   *
   * @param string $key
   */
  public static function start(string $key = 'default'): void {
    if (isset(self::$timers[$key])) {
      self::$timers[$key]->start();
    }
    else {
      self::$timers[$key] = new Stopwatch();
    }
  }

  /**
   * Resets a specific timer, or default timer if not passed the $key parameter.
   * To reset all timers, call @see \Ayesh\PHP_Timer\Timer::resetAll().
   * @param string $key
   */
  public static function reset(string $key = 'default'): void {
    unset(self::$timers[$key]);
  }

  /**
   * Resets ALL timers.
   * To reset a specific timer, @see \Ayesh\PHP_Timer\Timer::reset().
   */
  public static function resetAll(): void {
    self::$timers = [];
  }

  /**
   * Formats the given time the processor into the given format.
   * @param float $value
   * @param string|bool $format
   * @return string
   */
  private static function formatTime(float $value, $format): string {
    switch ($format) {

      case static::FORMAT_PRECISE:
        return (string) ($value * 1000);

      case static::FORMAT_MILLISECONDS:
        return (string) round($value * 1000, 2);

      case static::FORMAT_SECONDS:
        return (string) round($value, 3);

      default:
        return (string) ($value * 1000);
    }
  }

  /**
   * Returns the time elapsed in the format requested in the $format parameter.
   * To access a specific timer, pass the same key that
   * @see \Ayesh\PHP_Timer\Timer::start() was called with. If the timer was not
   * started, a \LogicException will be thrown.
   *
   * The default format is milliseconds. See the class constants for additional
   * formats.
   *
   * @param string $key The key that the timer was started with. Default value is
   *   "default" throughout the class.
   * @param string $format
   * @return mixed The formatted time.
   * @throws \LogicException
   */
  public static function read(string $key = 'default', $format = self::FORMAT_MILLISECONDS) {
    if (isset(self::$timers[$key])) {
      return self::formatTime(self::$timers[$key]->read(), $format);
    }
    throw new \LogicException('Reading timer when the given key timer was not initialized.');
  }

  /**
   * Stops the timer with the given key. Default key is "default"
   *
   * @param string $key
   *
   * @throws \LogicException If the attempted timer has not started already.
   */
  public static function stop($key = 'default'): void {
    if (isset(self::$timers[$key])) {
      self::$timers[$key]->stop();
    } else {
      throw new \LogicException('Stopping timer when the given key timer was not initialized.');
    }
  }

  /**
   * Return a list of timer names. Note that resetting a timer removes the timer.
   * @return string[]
   */
  public static function getTimers(): array {
    return array_keys(self::$timers);
  }
}


<?php declare(strict_types = 1);

/**
 * This file is part of the Nextras\Dbal library.
 * @license    MIT
 * @link       https://github.com/nextras/dbal
 */

namespace Nextras\Dbal\Result;

use DateTimeZone;
use Nextras\Dbal\Drivers\IDriver;
use Nextras\Dbal\Drivers\IResultAdapter;
use Nextras\Dbal\InvalidArgumentException;
use Nextras\Dbal\Utils\DateTimeImmutable;

class Result implements \SeekableIterator, \Countable
{
	/** @var IResultAdapter */
	private $adapter;

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

	/** @var Row|null */
	private $iteratorRow;

	/** @var IDriver */
	private $driver;

	/** @var string[] list of columns which should be casted to int */
	private $toIntColumns;

	/** @var string[] list of columns which should be casted to float */
	private $toFloatColumns;

	/** @var string[] list of columns which should be casted to string */
	private $toStringColumns;

	/** @var string[] list of columns which should be casted to bool */
	private $toBoolColumns;

	/** @var string[] list of columns which should be casted to DateTime */
	private $toDateTimeColumns;

	/** @var array[] list of columns which should be casted using driver-specific logic */
	private $toDriverColumns;

	/** @var DateTimeZone */
	private $applicationTimeZone;

	public function __construct(IResultAdapter $adapter, IDriver $driver)
	{
		$this->adapter = $adapter;
		$this->driver = $driver;
		$this->applicationTimeZone = new DateTimeZone(date_default_timezone_get());
		$this->initColumnConversions();
	}

	public function getAdapter(): IResultAdapter
	{
		return $this->adapter;
	}

	/**
	 * Enables and disables value normalization.
	 */
	public function setValueNormalization(bool $enabled = false)
	{
		if ($enabled === true) {
			$this->initColumnConversions();
		} else {
			$this->toIntColumns = [];
			$this->toFloatColumns = [];
			$this->toStringColumns = [];
			$this->toBoolColumns = [];
			$this->toDateTimeColumns = [];
			$this->toDriverColumns = [];
		}
	}

	public function fetch(): ?Row
	{
		$data = $this->adapter->fetch();
		$row = ($data === null ? null : new Row($this->normalize($data)));
		$this->iteratorIndex++;
		return $this->iteratorRow = $row;
	}

	/**
	 * @return mixed|null
	 */
	public function fetchField(int $column = 0)
	{
		if ($row = $this->fetch()) { // = intentionally
			return $row->getNthField($column);
		}

		return null;
	}

	/**
	 * @return Row[]
	 */
	public function fetchAll(): array
	{
		return iterator_to_array($this);
	}

	public function fetchPairs(string $key = null, string $value = null): array
	{
		if ($key === null && $value === null) {
			throw new InvalidArgumentException('Result::fetchPairs() requires defined key or value.');
		}

		$return = [];
		$this->seek(0);

		if ($key === null) {
			while ($row = $this->fetch()) {
				$return[] = $row->{$value};
			}
		} elseif ($value === null) {
			while ($row = $this->fetch()) {
				$return[($row->{$key} instanceof DateTimeImmutable) ? (string) $row->{$key} : $row->{$key}] = $row;
			}
		} else {
			while ($row = $this->fetch()) {
				$return[($row->{$key} instanceof DateTimeImmutable) ? (string) $row->{$key} : $row->{$key}] = $row->{$value};
			}
		}

		return $return;
	}

	protected function initColumnConversions()
	{
		$this->toIntColumns = [];
		$this->toFloatColumns = [];
		$this->toStringColumns = [];
		$this->toBoolColumns = [];
		$this->toDateTimeColumns = [];
		$this->toDriverColumns = [];

		$types = $this->adapter->getTypes();
		foreach ($types as $key => $typePair) {
			list($type, $nativeType) = $typePair;

			if ($type & IResultAdapter::TYPE_STRING) {
				$this->toStringColumns[] = $key;

			} elseif ($type & IResultAdapter::TYPE_INT) {
				$this->toIntColumns[] = $key;

			} elseif ($type & IResultAdapter::TYPE_FLOAT) {
				$this->toFloatColumns[] = $key;

			} elseif ($type & IResultAdapter::TYPE_BOOL) {
				$this->toBoolColumns[] = $key;

			} elseif ($type & IResultAdapter::TYPE_DATETIME) {
				$this->toDateTimeColumns[] = $key;
			}

			if ($type & IResultAdapter::TYPE_DRIVER_SPECIFIC) {
				$this->toDriverColumns[] = [$key, $nativeType];
			}
		}
	}

	protected function normalize(array $data): array
	{
		foreach ($this->toDriverColumns as $meta) {
			list($column, $nativeType) = $meta;
			if ($data[$column] !== null) {
				$data[$column] = $this->driver->convertToPhp($data[$column], $nativeType);
			}
		}

		foreach ($this->toIntColumns as $column) {
			if ($data[$column] !== null) {
				$data[$column] = (int) $data[$column];
			}
		}

		foreach ($this->toFloatColumns as $column) {
			if ($data[$column] !== null) {
				$data[$column] = (float) $data[$column];
			}
		}

		foreach ($this->toBoolColumns as $column) {
			if ($data[$column] !== null) {
				$data[$column] = (bool) $data[$column];
			}
		}

		foreach ($this->toStringColumns as $column) {
			if ($data[$column] !== null) {
				$data[$column] = (string) $data[$column];
			}
		}

		foreach ($this->toDateTimeColumns as $column) {
			if ($data[$column] !== null) {
				$data[$column] = (new DateTimeImmutable($data[$column]))->setTimezone($this->applicationTimeZone);
			}
		}

		return $data;
	}

	// === SeekableIterator ============================================================================================

	public function key()
	{
		return $this->iteratorIndex;
	}

	public function current()
	{
		return $this->iteratorRow;
	}

	public function next()
	{
		$this->fetch();
	}

	public function valid()
	{
		return $this->iteratorRow !== null;
	}

	public function rewind()
	{
		$this->seek(0);
		$this->fetch();
	}

	public function seek($index)
	{
		$this->adapter->seek($index);
		$this->iteratorIndex = $index - 1;
	}

	// === Countable ===================================================================================================

	public function count()
	{
		return $this->adapter->getRowsCount();
	}
}


<?php declare(strict_types = 1);

/**
 * This file is part of the Nextras\Dbal library.
 * @license    MIT
 * @link       https://github.com/nextras/dbal
 */

namespace Nextras\Dbal\QueryBuilder;

use Nextras\Dbal\Drivers\IDriver;
use Nextras\Dbal\InvalidArgumentException;
use Nextras\Dbal\InvalidStateException;

class QueryBuilder
{
	/** @const */
	const TYPE_SELECT = 1;
	const TYPE_INSERT = 2;
	const TYPE_UPDATE = 3;
	const TYPE_DELETE = 4;

	/** @var IDriver */
	private $driver;

	/** @var int */
	private $type = self::TYPE_SELECT;

	/** @var array */
	private $args = [
		'select' => null,
		'from' => null,
		'join' => null,
		'where' => null,
		'group' => null,
		'having' => null,
		'order' => null,
	];

	/** @var array|null */
	private $select;

	/** @var array|null */
	private $from;

	/** @var array|null */
	private $join;

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

	/** @var array|null */
	private $group;

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

	/** @var array|null */
	private $order;

	/** @var array|null */
	private $limit;

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

	public function __construct(IDriver $driver)
	{
		$this->driver = $driver;
	}

	public function getQuerySql(): string
	{
		if ($this->generatedSql !== null) {
			return $this->generatedSql;
		}

		switch ($this->type) {
			case self::TYPE_SELECT:
			default:
				$sql = $this->getSqlForSelect();
				break;
		}

		$this->generatedSql = $sql;
		return $sql;
	}

	public function getQueryParameters(): array
	{
		return array_merge(
			(array) $this->args['select'],
			(array) $this->args['from'],
			(array) $this->args['join'],
			(array) $this->args['where'],
			(array) $this->args['group'],
			(array) $this->args['having'],
			(array) $this->args['order']
		);
	}

	private function getSqlForSelect(): string
	{
		$query =
			'SELECT ' . ($this->select !== null ? implode(', ', $this->select) : '*')
			. ' FROM ' . $this->getFromClauses()
			. ($this->where !== null  ? ' WHERE ' . ($this->where) : '')
			. ($this->group           ? ' GROUP BY ' . implode(', ', $this->group) : '')
			. ($this->having !== null ? ' HAVING ' . ($this->having) : '')
			. ($this->order           ? ' ORDER BY ' . implode(', ', $this->order) : '');

		if ($this->limit) {
			$query = $this->driver->modifyLimitQuery($query, $this->limit[0], $this->limit[1]);
		}

		return $query;
	}

	private function getFromClauses(): string
	{
		$knownAliases = array_flip($this->getKnownAliases());

		$query = $this->from[0] . ($this->from[1] ? " AS [{$this->from[1]}]" : '');
		foreach ((array) $this->join as $join) {
			if (!isset($knownAliases[$join['from']])) {
				throw new InvalidStateException("Unknown alias '{$join['from']}'.");
			}

			$query .= ' '
				. $join['type'] . " JOIN {$join['table']} " . ($join['alias'] ? "AS [{$join['alias']}] " : '')
				. 'ON (' . $join['on'] . ')';
		}

		return $query;
	}

	public function getClause(string $part): array
	{
		if (!isset($this->args[$part]) && !array_key_exists($part, $this->args)) {
			throw new InvalidArgumentException("Unknown '$part' clause type.");
		}

		return [$this->$part, $this->args[$part]];
	}

	public function from(string $fromExpression, ?string $alias = null, ...$args): self
	{
		$this->dirty();
		$this->type = self::TYPE_SELECT;
		$this->from = [$fromExpression, $alias];
		$this->pushArgs('from', $args);
		return $this;
	}

	public function getFromAlias()
	{
		if ($this->from === null) {
			throw new InvalidStateException('From clause has not been set.');
		}

		return $this->from[1];
	}

	public function innerJoin(string $fromAlias, string $toExpression, string $toAlias, string $onExpression, ...$args): self
	{
		return $this->join('INNER', $fromAlias, $toExpression, $toAlias, $onExpression, $args);
	}

	public function leftJoin(string $fromAlias, string $toExpression, string $toAlias, string $onExpression, ...$args): self
	{
		return $this->join('LEFT', $fromAlias, $toExpression, $toAlias, $onExpression, $args);
	}

	public function rightJoin(string $fromAlias, string $toExpression, string $toAlias, string $onExpression, ...$args): self
	{
		return $this->join('RIGHT', $fromAlias, $toExpression, $toAlias, $onExpression, $args);
	}

	public function getJoin(string $toAlias)
	{
		return isset($this->join[$toAlias]) ? $this->join[$toAlias] : null;
	}

	private function join(string $type, string $fromAlias, string $toExpression, string $toAlias, string $onExpression, array $args): self
	{
		$this->dirty();
		$this->join[$toAlias] = [
			'type' => $type,
			'from' => $fromAlias,
			'table' => $toExpression,
			'alias' => $toAlias,
			'on' => $onExpression,
		];
		$this->pushArgs('join', $args);
		return $this;
	}

	/**
	 * Sets expression as SELECT clause. Passing null sets clause to the default state.
	 */
	public function select(?string $expression = null, ...$args): self
	{
		$this->dirty();
		$this->select = $expression === null ? null : [$expression];
		$this->args['select'] = $args;
		return $this;
	}

	/**
	 * Adds expression to SELECT clause.
	 */
	public function addSelect(string $expression, ...$args): self
	{
		if (!is_string($expression)) {
			throw new InvalidArgumentException('Select expression has to be a string.');
		}
		$this->dirty();
		$this->select[] = $expression;
		$this->pushArgs('select', $args);
		return $this;
	}

	/**
	 * Sets expression as WHERE clause. Passing null sets clause to the default state.
	 */
	public function where(?string $expression = null, ...$args): self
	{
		$this->dirty();
		$this->where = $expression;
		$this->args['where'] = $args;
		return $this;
	}

	/**
	 * Adds expression with AND to WHERE clause.
	 */
	public function andWhere(string $expression, ...$args): self
	{
		$this->dirty();
		$this->where = $this->where ? '(' . $this->where . ') AND (' . $expression . ')' : $expression;
		$this->pushArgs('where', $args);
		return $this;
	}

	/**
	 * Adds expression with OR to WHERE clause.
	 */
	public function orWhere(string $expression, ...$args): self
	{
		$this->dirty();
		$this->where = $this->where ? '(' . $this->where . ') OR (' . $expression . ')' : $expression;
		$this->pushArgs('where', $args);
		return $this;
	}

	/**
	 * Sets expression as GROUP BY clause. Passing null sets clause to the default state.
	 */
	public function groupBy(?string $expression = null, ...$args): self
	{
		$this->dirty();
		$this->group = $expression === null ? null : [$expression];
		$this->args['group'] = $args;
		return $this;
	}

	/**
	 * Adds expression to GROUP BY clause.
	 */
	public function addGroupBy($expression, ...$args): self
	{
		$this->dirty();
		$this->group[] = $expression;
		$this->pushArgs('group', $args);
		return $this;
	}

	/**
	 * Sets expression as HAVING clause. Passing null sets clause to the default state.
	 */
	public function having(?string $expression = null, ...$args): self
	{
		$this->dirty();
		$this->having = $expression;
		$this->args['having'] = $args;
		return $this;
	}

	/**
	 * Adds expression with AND to HAVING clause.
	 */
	public function andHaving(string $expression, ...$args): self
	{
		$this->dirty();
		$this->having = $this->having ? '(' . $this->having . ') AND (' . $expression . ')' : $expression;
		$this->pushArgs('having', $args);
		return $this;
	}

	/**
	 * Adds expression with OR to HAVING clause.
	 */
	public function orHaving(string $expression, ...$args): self
	{
		$this->dirty();
		$this->having = $this->having ? '(' . $this->having . ') OR (' . $expression . ')' : $expression;
		$this->pushArgs('having', $args);
		return $this;
	}

	/**
	 * Sets expression as ORDER BY clause. Passing null sets clause to the default state.
	 */
	public function orderBy(?string $expression = null, ...$args): self
	{
		$this->dirty();
		$this->order = $expression === null ? null : [$expression];
		$this->args['order'] = $args;
		return $this;
	}

	/**
	 * Adds expression to ORDER BY clause.
	 */
	public function addOrderBy(string $expression, ...$args): self
	{
		$this->dirty();
		$this->order[] = $expression;
		$this->pushArgs('order', $args);
		return $this;
	}

	/**
	 * Sets LIMIT and OFFSET clause.
	 */
	public function limitBy(?int $limit, int $offset = null): self
	{
		$this->dirty();
		$this->limit = $limit || $offset ? [$limit, $offset] : null;
		return $this;
	}

	/**
	 * Returns true if LIMIT or OFFSET clause is set.
	 */
	public function hasLimitOffsetClause(): bool
	{
		return $this->limit !== null;
	}

	/**
	 * Returns limit and offset clause arguments.
	 */
	public function getLimitOffsetClause(): ?array
	{
		return $this->limit;
	}

	private function dirty()
	{
		$this->generatedSql = null;
	}

	private function pushArgs($type, array $args)
	{
		$this->args[$type] = array_merge((array) $this->args[$type], $args);
	}

	/**
	 * @return string[]
	 */
	private function getKnownAliases(): array
	{
		$knownAliases = [];
		if (isset($this->from)) {
			$knownAliases[] = isset($this->from[1]) ? $this->from[1] : $this->from[0];
		}
		foreach ((array) $this->join as $join) {
			$knownAliases[] = isset($join['alias']) ? $join['alias'] : $join['table'];
		}

		return $knownAliases;
	}
}