<?php

namespace Netgen\Bundle\AdminUIBundle\DependencyInjection;

use Netgen\TagsBundle\Version as TagsBundleVersion;
use RuntimeException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Yaml\Yaml;

class NetgenAdminUIExtension extends Extension implements PrependExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader(
            $container,
            new FileLocator(__DIR__ . '/../Resources/config')
        );

        $loader->load('parameters.yml');
        $loader->load('menu_plugins.yml');
        $loader->load('templating.yml');
        $loader->load('controllers.yml');
        $loader->load('services.yml');

        $activatedBundles = $container->getParameter('kernel.bundles');

        if (!array_key_exists('EzCoreExtraBundle', $activatedBundles)) {
            throw new RuntimeException('Netgen Admin UI Bundle requires EzCoreExtraBundle (lolautruche/ez-core-extra-bundle) to be activated to work properly.');
        }

        if ($this->hasTags($activatedBundles)) {
            $loader->load('tags/services.yml');
        }

        if ($this->hasLayouts($activatedBundles)) {
            $loader->load('layouts/controllers.yml');
            $loader->load('layouts/services.yml');
            $this->setIsLayoutsEnterprise($container, $activatedBundles);
        }

        if ($this->hasInformationCollection($activatedBundles)) {
            $loader->load('information_collection/services.yml');
        }

        $logoType = $container->getParameter('netgen_admin_ui.logo_type');
        if ($logoType === 'default') {
            if (class_exists('Netgen\Bundle\SiteBundle\NetgenSiteBundle') || class_exists('Netgen\Bundle\MoreBundle\NetgenMoreBundle')) {
                $container->setParameter('netgen_admin_ui.logo_type', 'ngadminui');
            }
        }
    }

    /**
     * Allow an extension to prepend the extension configurations.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     */
    public function prepend(ContainerBuilder $container)
    {
        $configs = array(
            'framework/twig.yml' => 'twig',
        );

        if ($this->hasLayouts($container->getParameter('kernel.bundles'))) {
            $configs['layouts/view.yml'] = 'netgen_block_manager';
        }

        foreach ($configs as $fileName => $extensionName) {
            $configFile = __DIR__ . '/../Resources/config/' . $fileName;
            $config = Yaml::parse(file_get_contents($configFile));
            $container->prependExtensionConfig($extensionName, $config);
            $container->addResource(new FileResource($configFile));
        }
    }

    /**
     * Returns if Netgen Layouts is active or not.
     *
     * @param array $activatedBundles
     *
     * @return bool
     */
    protected function hasLayouts(array $activatedBundles)
    {
        return array_key_exists('NetgenBlockManagerBundle', $activatedBundles);
    }

    /**
     * Sets the flag to the container if Netgen Layouts is the enterprise or open source version.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     * @param array $activatedBundles
     */
    protected function setIsLayoutsEnterprise(ContainerBuilder $container, array $activatedBundles)
    {
        $container->setParameter(
            'netgen_admin_ui.layouts.is_enterprise',
            array_key_exists('NetgenLayoutsEnterpriseBundle', $activatedBundles)
        );
    }

    /**
     * Returns if Netgen Tags v3+ is active or not.
     *
     * @param array $activatedBundles
     *
     * @return bool
     */
    protected function hasTags(array $activatedBundles)
    {
        if (!class_exists('Netgen\TagsBundle\Version')) {
            return false;
        }

        if (TagsBundleVersion::MAJOR_VERSION < 3) {
            return false;
        }

        return array_key_exists('NetgenTagsBundle', $activatedBundles);
    }

    /**
     * Returns if Netgen Information Collection is active or not.
     *
     * @param array $activatedBundles
     *
     * @return bool
     */
    protected function hasInformationCollection(array $activatedBundles)
    {
        if (!array_key_exists('NetgenInformationCollectionBundle', $activatedBundles)) {
            return false;
        }
        // this service is backend for collected info admin
        // if service exists then plugin should be available
        return interface_exists('Netgen\Bundle\InformationCollectionBundle\API\Service\InformationCollection');
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Installer\Generator;

use eZ\Publish\API\Repository\Values\Content\Language;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Yaml;

class ConfigurationGenerator extends Generator
{
    /**
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
     */
    protected $container;

    /**
     * @var \Symfony\Component\Console\Helper\QuestionHelper
     */
    protected $questionHelper;

    /**
     * Constructor.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
     * @param \Symfony\Component\Console\Helper\QuestionHelper $questionHelper
     */
    public function __construct(ContainerInterface $container, QuestionHelper $questionHelper)
    {
        $this->container = $container;
        $this->questionHelper = $questionHelper;
    }

    /**
     * Generates the main configuration.
     *
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    public function generate(InputInterface $input, OutputInterface $output)
    {
        $fileSystem = $this->container->get('filesystem');
        $configResolver = $this->container->get('ezpublish.config.resolver');
        $kernelRootDir = $this->container->getParameter('kernel.root_dir');

        $siteAccessGroup = $input->getOption('site-access-group');

        $varDir = $configResolver->getParameter('var_dir', null, $siteAccessGroup);
        $repository = $configResolver->getParameter('repository', null, $siteAccessGroup);

        $configFile = $kernelRootDir . '/config/ngadminui.yml';

        if ($fileSystem->exists($configFile)) {
            if (
                !$this->questionHelper->ask(
                    $input,
                    $output,
                    new ConfirmationQuestion(
                        '<info><comment>ngadminui.yml</comment> configuration file already exists. Do you want to overwrite it?</info> [<comment>no</comment>] ',
                        false
                    )
                )
            ) {
                return;
            }
        }

        $siteAccessName = $input->getOption('site-access-name');

        $languageService = $this->container->get('ezpublish.api.repository')->getContentLanguageService();
        $languages = $languageService->loadLanguages();

        $settings = array(
            'parameters' => array(
                'netgen_admin_ui.' . $siteAccessName . '.is_admin_ui_siteaccess' => true,
                'eztags.' . $siteAccessName . '.routing.enable_tag_router' => false,
                'ezsettings.' . $siteAccessName . '.treemenu.http_cache' => false,
            ),
            'ezpublish' => array(
                'siteaccess' => array(
                    'list' => array(
                        $siteAccessName,
                    ),
                    'groups' => array(
                        'ngadminui' => array(
                            $siteAccessName,
                        ),
                    ),
                    'match' => array(
                        'Map\URI' => array(
                            $siteAccessName => $siteAccessName,
                        ),
                    ),
                ),
                'system' => array(
                    $siteAccessName => array(
                        'user' => array(
                            'layout' => '@NetgenAdminUI/pagelayout_login.html.twig',
                            'login_template' => '@NetgenAdminUI/user/login.html.twig',
                        ),
                        'languages' => array_map(
                            function (Language $language) {
                                return $language->languageCode;
                            },
                            $languages
                        ),
                        'var_dir' => $varDir,
                        'repository' => $repository,
                    ),
                ),
            ),
            'ez_publish_legacy' => array(
                'system' => array(
                    $siteAccessName => array(
                        'templating' => array(
                            'view_layout' => '@NetgenAdminUI/pagelayout_legacy.html.twig',
                            'module_layout' => '@NetgenAdminUI/pagelayout_module.html.twig',
                        ),
                    ),
                ),
            ),
        );

        file_put_contents($configFile, Yaml::dump($settings, 7));

        $output->writeln(
            array(
                '',
                'Generated <comment>ngadminui.yml</comment> configuration file!',
                '',
            )
        );
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\MenuPlugin;

use eZ\Bundle\EzPublishLegacyBundle\Routing\FallbackRouter;
use eZ\Publish\Core\MVC\Symfony\Routing\UrlAliasRouter;
use Symfony\Component\HttpFoundation\Request;

class LegacyMenuPlugin implements MenuPluginInterface
{
    /**
     * Returns plugin identifier.
     *
     * @return string
     */
    public function getIdentifier()
    {
        return 'legacy';
    }

    /**
     * Returns the list of templates this plugin supports.
     *
     * @return array
     */
    public function getTemplates()
    {
        return array(
            'aside' => '@NetgenAdminUI/menu/plugins/legacy/aside.html.twig',
            'left' => '@NetgenAdminUI/menu/plugins/legacy/left.html.twig',
            'top' => '@NetgenAdminUI/menu/plugins/legacy/top.html.twig',
        );
    }

    /**
     * Returns if the menu is active.
     *
     * @return bool
     */
    public function isActive()
    {
        return true;
    }

    /**
     * Returns if this plugin matches the current request.
     *
     * @param \Symfony\Component\HttpFoundation\Request $request
     *
     * @return bool
     */
    public function matches(Request $request)
    {
        return in_array(
            $request->attributes->get('_route'),
            array(FallbackRouter::ROUTE_NAME, UrlAliasRouter::URL_ALIAS_ROUTE_NAME),
            true
        );
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Templating;

use Netgen\Bundle\AdminUIBundle\MenuPlugin\MenuPluginRegistry;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

class GlobalVariable
{
    /**
     * @var \Netgen\Bundle\AdminUIBundle\MenuPlugin\MenuPluginRegistry
     */
    protected $menuPluginRegistry;

    /**
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    protected $requestStack;

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

    /**
     * Constructor.
     *
     * @param \Netgen\Bundle\AdminUIBundle\MenuPlugin\MenuPluginRegistry $menuPluginRegistry
     * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
     * @param string $logoType
     */
    public function __construct(
        MenuPluginRegistry $menuPluginRegistry,
        RequestStack $requestStack,
        $logoType
    ) {
        $this->menuPluginRegistry = $menuPluginRegistry;
        $this->requestStack = $requestStack;
        $this->logoType = $logoType;
    }

    /**
     * Returns all menu plugins.
     *
     * @return \Netgen\Bundle\AdminUIBundle\MenuPlugin\MenuPluginInterface[]
     */
    public function getMenuPlugins()
    {
        return $this->menuPluginRegistry->getMenuPlugins();
    }

    /**
     * Returns the logo type.
     *
     * @return string
     */
    public function getLogoType()
    {
        return $this->logoType;
    }

    /**
     * Returns the identifier of the current menu plugin.
     *
     * @return string
     */
    public function getCurrentMenuPlugin()
    {
        $currentRequest = $this->requestStack->getCurrentRequest();
        if (!$currentRequest instanceof Request) {
            return false;
        }

        foreach ($this->menuPluginRegistry->getMenuPlugins() as $identifier => $menuPlugin) {
            if ($menuPlugin->matches($currentRequest)) {
                return $identifier;
            }
        }

        return false;
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;

class ControllerListener implements EventSubscriberInterface
{
    /**
     * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface
     */
    protected $controllerResolver;

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

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

    /**
     * Constructor.
     *
     * @param \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface $controllerResolver
     * @param bool $isAdminSiteAccess
     * @param array $legacyRoutes
     */
    public function __construct(
        ControllerResolverInterface $controllerResolver,
        $isAdminSiteAccess = false,
        $legacyRoutes = array()
    ) {
        $this->controllerResolver = $controllerResolver;
        $this->isAdminSiteAccess = $isAdminSiteAccess;
        $this->legacyRoutes = $legacyRoutes;
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array The event names to listen to
     */
    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::CONTROLLER => array('onKernelController', 255),
        );
    }

    /**
     * Redirects configured routes to eZ legacy.
     *
     * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) {
            return;
        }

        if (!$this->isAdminSiteAccess) {
            return;
        }

        $currentRoute = $event->getRequest()->attributes->get('_route');

        foreach ($this->legacyRoutes as $legacyRoute) {
            if (stripos($currentRoute, $legacyRoute) === 0) {
                $event->getRequest()->attributes->set('_controller', 'ezpublish_legacy.controller:indexAction');
                $event->setController($this->controllerResolver->getController($event->getRequest()));

                return;
            }
        }
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Installer\Generator;

use Twig_Environment;
use Twig_Loader_Filesystem;

abstract class Generator
{
    /**
     * @var array
     */
    protected $skeletonDirs;

    /**
     * Sets an array of directories to look for templates.
     *
     * The directories must be sorted from the most specific to the least specific directory
     *
     * @param array|string $skeletonDirs An array of skeleton dirs
     */
    public function setSkeletonDirs($skeletonDirs)
    {
        $this->skeletonDirs = is_array($skeletonDirs) ? $skeletonDirs : array($skeletonDirs);
    }

    /**
     * Renders the template.
     *
     * @param string $template
     * @param array $parameters
     *
     * @return string
     */
    protected function render($template, $parameters)
    {
        $twig = new Twig_Environment(
            new Twig_Loader_Filesystem($this->skeletonDirs),
            array(
                'debug' => true,
                'cache' => false,
                'strict_variables' => true,
                'autoescape' => false,
            )
        );

        return $twig->render($template, $parameters);
    }

    /**
     * Renders the template to a file.
     *
     * @param string $template
     * @param string $target
     * @param array $parameters
     *
     * @return int
     */
    protected function renderFile($template, $target, $parameters)
    {
        if (!is_dir(dirname($target))) {
            mkdir(dirname($target), 0777, true);
        }

        return file_put_contents($target, $this->render($template, $parameters));
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Layouts\EventListener;

use Netgen\BlockManager\Event\BlockManagerEvents;
use Netgen\BlockManager\Event\CollectViewParametersEvent;
use Netgen\BlockManager\View\View\LayoutViewInterface;
use Netgen\BlockManager\View\View\RuleViewInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class IsEnterpriseVersionListener implements EventSubscriberInterface
{
    /**
     * @var bool
     */
    private $isEnterpriseVersion;

    public function __construct(bool $isEnterpriseVersion)
    {
        $this->isEnterpriseVersion = $isEnterpriseVersion;
    }

    public static function getSubscribedEvents()
    {
        return array(BlockManagerEvents::BUILD_VIEW => 'onBuildView');
    }

    /**
     * Injects if Netgen Layouts is the enterprise version or not.
     */
    public function onBuildView(CollectViewParametersEvent $event)
    {
        $view = $event->getView();
        if (!$view instanceof LayoutViewInterface && !$view instanceof RuleViewInterface) {
            return;
        }

        if ($view->getContext() !== 'ngadminui') {
            return;
        }

        $event->addParameter('is_enterprise', $this->isEnterpriseVersion);
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class MenuPluginRegistryPass implements CompilerPassInterface
{
    /**
     * Registers all menu plugins in the plugin registry.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->has('netgen_admin_ui.menu_plugin.registry')) {
            return;
        }

        $menuPluginRegistry = $container->findDefinition('netgen_admin_ui.menu_plugin.registry');
        $menuPlugins = $container->findTaggedServiceIds('netgen_admin_ui.menu_plugin');

        $flattenedMenuPlugins = array();
        foreach ($menuPlugins as $identifier => $menuPlugin) {
            $flattenedMenuPlugins[$identifier] = isset($menuPlugin[0]['priority']) ? $menuPlugin[0]['priority'] : 0;
        }

        arsort($flattenedMenuPlugins);

        foreach (array_keys($flattenedMenuPlugins) as $menuPlugin) {
            $menuPluginRegistry->addMethodCall(
                'addMenuPlugin',
                array(new Reference($menuPlugin))
            );
        }
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Templating\Twig\Extension;

use Closure;
use eZPreferences;
use Netgen\Bundle\AdminUIBundle\Helper\PathHelper;
use Twig_Extension;
use Twig_SimpleFunction;

class NetgenAdminUIExtension extends Twig_Extension
{
    /**
     * @var \Netgen\Bundle\AdminUIBundle\Helper\PathHelper
     */
    protected $pathHelper;

    /**
     * @var \Closure
     */
    protected $legacyKernel;

    /**
     * Constructor.
     *
     * @param \Netgen\Bundle\AdminUIBundle\Helper\PathHelper $pathHelper
     * @param \Closure $legacyKernel
     */
    public function __construct(PathHelper $pathHelper, Closure $legacyKernel)
    {
        $this->pathHelper = $pathHelper;
        $this->legacyKernel = $legacyKernel;
    }

    /**
     * Returns the name of the extension.
     *
     * @return string The extension name
     */
    public function getName()
    {
        return 'netgen_admin_ui';
    }

    /**
     * Returns a list of functions to add to the existing list.
     *
     * @return array An array of functions
     */
    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction(
                'ngadmin_location_path',
                array($this, 'getLocationPath'),
                array('is_safe' => array('html'))
            ),
            new Twig_SimpleFunction(
                'ngadmin_ezpreference',
                array($this, 'getLegacyPreference')
            ),
        );
    }

    /**
     * Returns the path for specified location ID.
     *
     * @param mixed $locationId
     *
     * @return array
     */
    public function getLocationPath($locationId)
    {
        return $this->pathHelper->getPath($locationId);
    }

    /**
     * Returns eZ Publish Legacy ezpreference value.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function getLegacyPreference($name)
    {
        $legacyKernel = $this->legacyKernel;

        return $legacyKernel()->runCallback(
            function () use ($name) {
                return eZPreferences::value($name);
            }
        );
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\EventListener;

use eZ\Bundle\EzPublishLegacyBundle\Routing\FallbackRouter;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;

class LegacyExceptionListener implements EventSubscriberInterface
{
    /**
     * @var bool
     */
    protected $isAdminSiteAccess;

    /**
     * Constructor.
     *
     * @param bool $isAdminSiteAccess
     */
    public function __construct($isAdminSiteAccess = false)
    {
        $this->isAdminSiteAccess = $isAdminSiteAccess;
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return array(KernelEvents::EXCEPTION => 'onException');
    }

    /**
     * Handles the legacy exceptions.
     *
     * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
     */
    public function onException(GetResponseForExceptionEvent $event)
    {
        if (!$this->isAdminSiteAccess) {
            return;
        }

        $routeName = $event->getRequest()->attributes->get('_route');
        if ($routeName !== FallbackRouter::ROUTE_NAME) {
            return;
        }

        $exception = $event->getException();
        if (!$exception instanceof NotFoundHttpException) {
            return;
        }

        // We're supporting admin UI exception as well as Netgen
        // internal exception
        if (method_exists($exception, 'getOriginalResponse')) {
            $event->setResponse($exception->getOriginalResponse());
        }
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\EventListener;

use Netgen\Bundle\InformationCollectionBundle\Templating\Twig\AdminGlobalVariable;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SetInformationCollectionAdminPageLayoutListener implements EventSubscriberInterface
{
    /**
     * @var \Netgen\Bundle\InformationCollectionBundle\Templating\Twig\AdminGlobalVariable
     */
    protected $globalVariable;

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

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

    /**
     * Constructor.
     *
     * @param \Netgen\Bundle\InformationCollectionBundle\Templating\Twig\AdminGlobalVariable $globalVariable
     * @param string $pageLayoutTemplate
     */
    public function __construct(AdminGlobalVariable $globalVariable, $pageLayoutTemplate)
    {
        $this->globalVariable = $globalVariable;
        $this->pageLayoutTemplate = $pageLayoutTemplate;
    }

    /**
     * Sets if the current siteaccess will be considered as Netgen Admin UI siteaccess.
     *
     * @param bool $isAdminSiteAccess
     */
    public function setIsAdminSiteAccess($isAdminSiteAccess = false)
    {
        $this->isAdminSiteAccess = (bool) $isAdminSiteAccess;
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return array(KernelEvents::REQUEST => 'onKernelRequest');
    }

    /**
     * Sets the Netgen Tags admin pagelayout for Netgen Admin UI.
     *
     * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        if (!$this->isAdminSiteAccess) {
            return;
        }

        $currentRoute = $event->getRequest()->attributes->get('_route');
        if (mb_stripos($currentRoute, 'netgen_information_collection') !== 0) {
            return;
        }

        $this->globalVariable->setPageLayoutTemplate($this->pageLayoutTemplate);
    }
}


<?php

use \Netgen\TagsBundle\Version as TagsBundleVersion;

class NgAdminUiTagsBundleOperator
{
    /**
     * Returns the list of template operators this class supports
     *
     * @return array
     */
    function operatorList()
    {
        return array( 'has_tags_bundle' );
    }

    /**
     * Indicates if the template operators have named parameters
     *
     * @return bool
     */
    function namedParameterPerOperator()
    {
        return false;
    }

    /**
     * Executes the template operator
     *
     * @param eZTemplate $tpl
     * @param string $operatorName
     * @param mixed $operatorParameters
     * @param string $rootNamespace
     * @param string $currentNamespace
     * @param mixed $operatorValue
     * @param array $namedParameters
     * @param mixed $placement
     */
    public static function modify( $tpl, $operatorName, $operatorParameters, $rootNamespace, $currentNamespace, &$operatorValue, $namedParameters, $placement )
    {
        if ( $operatorName === 'has_tags_bundle' ) {
            $operatorValue = class_exists('Netgen\TagsBundle\Version') && TagsBundleVersion::VERSION_ID >= 30000;
        }
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\EventListener;

use eZ\Bundle\EzPublishLegacyBundle\LegacyResponse;
use eZ\Bundle\EzPublishLegacyBundle\Routing\FallbackRouter;
use Netgen\Bundle\AdminUIBundle\Exception\NotFoundHttpException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LegacyResponseListener implements EventSubscriberInterface
{
    /**
     * @var bool
     */
    protected $legacyMode;

    /**
     * Constructor.
     *
     * @param bool $legacyMode
     */
    public function __construct($legacyMode)
    {
        $this->legacyMode = $legacyMode;
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return array(KernelEvents::RESPONSE => 'onKernelResponse');
    }

    /**
     * Converts the legacy 404 response to proper Symfony exception.
     *
     *
     * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    public function onKernelResponse(FilterResponseEvent $event)
    {
        $routeName = $event->getRequest()->attributes->get('_route');
        if ($routeName !== FallbackRouter::ROUTE_NAME) {
            return;
        }

        $response = $event->getResponse();
        if (!$response instanceof LegacyResponse) {
            return;
        }

        if (!$this->legacyMode && (int) $response->getStatusCode() === Response::HTTP_NOT_FOUND) {
            $moduleResult = $response->getModuleResult();
            $exception = new NotFoundHttpException(
                isset($moduleResult['errorMessage']) ?
                    $moduleResult['errorMessage'] :
                    'Not Found'
            );

            $exception->setOriginalResponse($response);

            throw $exception;
        }
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Controller;

use eZ\Publish\API\Repository\Values\Content\Content;
use eZ\Publish\API\Repository\Values\Content\Location;
use eZ\Publish\Core\MVC\Symfony\View\ContentView;
use Netgen\BlockManager\Layout\Resolver\LayoutResolverInterface;
use Netgen\Bundle\AdminUIBundle\Layouts\RelatedLayoutsLoader;
use Symfony\Component\HttpFoundation\Request;

class LayoutsController extends Controller
{
    /**
     * @var \Netgen\BlockManager\Layout\Resolver\LayoutResolverInterface
     */
    protected $layoutResolver;

    /**
     * @var \Netgen\Bundle\AdminUIBundle\Layouts\RelatedLayoutsLoader
     */
    protected $relatedLayoutsLoader;

    public function __construct(
        LayoutResolverInterface $layoutResolver,
        RelatedLayoutsLoader $relatedLayoutsLoader
    ) {
        $this->layoutResolver = $layoutResolver;
        $this->relatedLayoutsLoader = $relatedLayoutsLoader;
    }

    /**
     * Renders a template that shows all layouts applied to provided location.
     *
     * @param int|string $locationId
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function showLocationLayouts($locationId)
    {
        $repository = $this->getRepository();

        $location = $repository->getLocationService()->loadLocation($locationId);
        $content = $repository->getContentService()->loadContent($location->contentInfo->id);

        $request = $this->createRequest($content, $location);

        return $this->render(
            '@NetgenAdminUI/layouts/location_layouts.html.twig',
            array(
                'rules' => $this->layoutResolver->resolveRules($request, array('ez_content_type')),
                'related_layouts' => $this->relatedLayoutsLoader->loadRelatedLayouts($location),
                'location' => $location,
            )
        );
    }

    /**
     * Creates the request used for fetching the mappings applied to provided content and location.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
     *
     * @return \Symfony\Component\HttpFoundation\Request
     */
    protected function createRequest(Content $content, Location $location)
    {
        $request = Request::create('');

        $request->attributes->set('content', $content);
        $request->attributes->set('location', $location);

        if (interface_exists('eZ\Publish\Core\MVC\Symfony\View\ContentValueView')) {
            $contentView = new ContentView();
            $contentView->setLocation($location);
            $contentView->setContent($content);

            $request->attributes->set('view', $contentView);
        }

        return $request;
    }

    /**
     * Performs access checks on the controller.
     */
    protected function checkPermissions()
    {
        if ($this->isGranted('ROLE_NGBM_EDITOR')) {
            return;
        }

        if ($this->isGranted('nglayouts:ui:access')) {
            return;
        }

        $exception = $this->createAccessDeniedException();
        $exception->setAttributes('nglayouts:ui:access');

        throw $exception;
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\EventListener;

use eZ\Publish\API\Repository\Repository;
use eZ\Publish\Core\MVC\ConfigResolverInterface;
use eZ\Publish\Core\MVC\Legacy\Event\PostBuildKernelEvent;
use eZ\Publish\Core\MVC\Legacy\LegacyEvents;
use ezpWebBasedKernelHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class SecurityListener implements EventSubscriberInterface
{
    /**
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    protected $requestStack;

    /**
     * @var \eZ\Publish\API\Repository\Repository
     */
    protected $repository;

    /**
     * @var \eZ\Publish\Core\MVC\ConfigResolverInterface
     */
    protected $configResolver;

    /**
     * @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
     */
    protected $tokenStorage;

    /**
     * @var \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
     */
    protected $authorizationChecker;

    /**
     * Constructor.
     *
     * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
     * @param \eZ\Publish\API\Repository\Repository $repository
     * @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver
     * @param \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $tokenStorage
     * @param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authorizationChecker
     */
    public function __construct(
        RequestStack $requestStack,
        Repository $repository,
        ConfigResolverInterface $configResolver,
        TokenStorageInterface $tokenStorage,
        AuthorizationCheckerInterface $authorizationChecker
    ) {
        $this->requestStack = $requestStack;
        $this->repository = $repository;
        $this->configResolver = $configResolver;
        $this->tokenStorage = $tokenStorage;
        $this->authorizationChecker = $authorizationChecker;
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return array(
            LegacyEvents::POST_BUILD_LEGACY_KERNEL => array('onKernelBuilt', 255),
        );
    }

    /**
     * Performs actions related to security once the legacy kernel has been built.
     *
     * @param \eZ\Publish\Core\MVC\Legacy\Event\PostBuildKernelEvent $event
     */
    public function onKernelBuilt(PostBuildKernelEvent $event)
    {
        $currentRequest = $this->requestStack->getCurrentRequest();

        // Ignore if not in web context, if legacy_mode is active or if user is not authenticated
        if (
            $currentRequest === null
            || !$event->getKernelHandler() instanceof ezpWebBasedKernelHandler
            || $this->configResolver->getParameter('legacy_mode') === true
            || !$this->isUserAuthenticated()
        ) {
            return;
        }

        /*
        Siteaccesses running with legacy_mode=false and RequireUserLogin=true have issues with
        login redirects when protected legacy views are accessed via URL before the user is logged in.
        This is because RequireUserLogin specific code in legacy kernel checks for existence of
        'eZUserLoggedInID' DURING the legacy kernel initialization process. Since 'eZUserLoggedInID'
        session variable can only be set AFTER the legacy kernel is built and initialized,
        via runCallback method, the code always fails, causing repeated login screens.

        This is a workaround to set the session variable after the kernel is built, but before
        it's initialized, so the RequireUserLogin code should work fine.
        */

        $currentRequest->getSession()->set(
            'eZUserLoggedInID',
            $this->repository->getCurrentUser()->id
        );
    }

    /**
     * @return bool
     */
    protected function isUserAuthenticated()
    {
        // IS_AUTHENTICATED_FULLY inherits from IS_AUTHENTICATED_REMEMBERED.
        // User can be either authenticated by providing credentials during current session
        // or by "remember me" if available.
        return
            $this->tokenStorage->getToken() instanceof TokenInterface
            && $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED');
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Installer\Generator;

use eZ\Publish\API\Repository\Values\Content\Language;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\DependencyInjection\ContainerInterface;

class LegacySiteAccessGenerator extends Generator
{
    /**
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
     */
    protected $container;

    /**
     * @var \Symfony\Component\Console\Helper\QuestionHelper
     */
    protected $questionHelper;

    /**
     * Constructor.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
     * @param \Symfony\Component\Console\Helper\QuestionHelper $questionHelper
     */
    public function __construct(ContainerInterface $container, QuestionHelper $questionHelper)
    {
        $this->container = $container;
        $this->questionHelper = $questionHelper;
    }

    /**
     * Generates the siteaccesses.
     *
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    public function generate(InputInterface $input, OutputInterface $output)
    {
        $fileSystem = $this->container->get('filesystem');
        $legacyRootDir = $this->container->getParameter('ezpublish_legacy.root_dir');

        $siteAccessName = $input->getOption('site-access-name');
        $languageCode = $input->getOption('language-code');
        $siteAccessLocation = $legacyRootDir . '/settings/siteaccess/' . $siteAccessName;
        $skeletonDir = __DIR__ . '/../_templates/legacy_siteaccess';

        if ($fileSystem->exists($siteAccessLocation)) {
            if (
                !$this->questionHelper->ask(
                    $input,
                    $output,
                    new ConfirmationQuestion(
                        '<info><comment>' . $siteAccessName . '</comment> legacy siteaccess already exists. Do you want to overwrite it?</info> [<comment>no</comment>] ',
                        false
                    )
                )
            ) {
                return;
            }
        }

        $fileSystem->remove($siteAccessLocation);

        // Template variables

        $languageService = $this->container->get('ezpublish.api.repository')->getContentLanguageService();

        $relatedSiteAccessList = $this->container->getParameter('ezpublish.siteaccess.list');
        $relatedSiteAccessList[] = $siteAccessName;

        $availableLocales = array_map(
            function (Language $language) {
                return $language->languageCode;
            },
            $languageService->loadLanguages()
        );

        $availableLocales = array_values(array_diff($availableLocales, array($languageCode)));

        // Place siteaccess locale at the top of site language list
        $siteLanguageList = array_merge(array($languageCode), $availableLocales);
        $translationList = implode(';', $availableLocales);

        // Generating admin siteaccess

        $fileSystem->mirror($skeletonDir, $siteAccessLocation);

        $this->setSkeletonDirs($siteAccessLocation);

        $this->renderFile(
            'site.ini.append.php',
            $siteAccessLocation . '/site.ini.append.php',
            array(
                'relatedSiteAccessList' => $relatedSiteAccessList,
                'siteAccessLocale' => $languageCode,
                'siteLanguageList' => $siteLanguageList,
                'translationList' => $translationList,
            )
        );

        $output->writeln(
            array(
                '',
                'Generated <comment>' . $siteAccessName . '</comment> legacy siteaccess!',
                '',
            )
        );
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Command;

use Exception;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use InvalidArgumentException;
use Netgen\Bundle\AdminUIBundle\Installer\Generator\ConfigurationGenerator;
use Netgen\Bundle\AdminUIBundle\Installer\Generator\LegacySiteAccessGenerator;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Process\ProcessBuilder;

class InstallCommand extends ContainerAwareCommand
{
    /**
     * @var \Symfony\Component\Console\Input\InputInterface
     */
    protected $input;

    /**
     * @var \Symfony\Component\Console\Output\OutputInterface
     */
    protected $output;

    /**
     * @var \Symfony\Component\Console\Helper\QuestionHelper
     */
    protected $questionHelper;

    /**
     * Configures the command.
     */
    protected function configure()
    {
        $this->setDefinition(
            array(
                new InputOption('site-access-name', '', InputOption::VALUE_REQUIRED, 'Siteaccess name'),
                new InputOption('language-code', '', InputOption::VALUE_REQUIRED, 'Language code'),
                new InputOption('site-access-group', '', InputOption::VALUE_REQUIRED, 'Siteaccess group'),
            )
        );
        $this->setDescription('Netgen Admin UI installation');
        $this->setName('ngadminui:install');
    }

    /**
     * Runs the command interactively.
     *
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     *
     * @return int
     */
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        $this->input = $input;
        $this->output = $output;
        $this->questionHelper = $this->getHelper('question');

        if (Kernel::VERSION_ID < 20700) {
            throw new RuntimeException(
                'Installation is not possible. Netgen Admin UI requires Symfony 2.7 or later to work.'
            );
        }

        if (!$this->getContainer()->hasParameter('ezpublish_legacy.root_dir')) {
            throw new RuntimeException(
                sprintf(
                    "%s\n%s",
                    'Installation is not possible because eZ Publish Legacy is not present.',
                    'Netgen Admin UI requires eZ Publish Community 2014.12 (Netgen Variant), eZ Publish 5.4.x or eZ Platform with Legacy Bridge to work.'
                )
            );
        }

        $this->writeSection('Welcome to the Netgen Admin UI installation');

        while (!$this->doInteract()) {
        }

        return 0;
    }

    /**
     * Collects all the data interactively.
     *
     * @return bool
     */
    protected function doInteract()
    {
        $siteAccess = $this->askForData(
            'site-access-name',
            'Enter the name of the Netgen Admin UI siteaccess',
            'ngadminui',
            function ($siteaccess) {
                if (!preg_match('/^[a-z][a-z0-9_]*$/', $siteaccess)) {
                    throw new InvalidArgumentException(
                        'Siteaccess name is not valid. It must start with a letter, followed by any combination of letters, numbers and underscore.'
                    );
                }

                $existingSiteAccesses = $this->getContainer()->getParameter('ezpublish.siteaccess.list');
                if (in_array($siteaccess, $existingSiteAccesses, true)) {
                    throw new InvalidArgumentException(
                        sprintf('Siteaccess "%s" already exists.', $siteaccess)
                    );
                }

                return $siteaccess;
            }
        );

        $this->output->writeln('');

        $languageCode = $this->askForData(
            'language-code',
            'Enter the language code in which the Netgen Admin UI will be translated',
            'eng-GB',
            function ($languageCode) {
                $languageService = $this->getContainer()->get('ezpublish.api.repository')->getContentLanguageService();

                try {
                    $languageService->loadLanguage($languageCode);
                } catch (NotFoundException $e) {
                    throw new InvalidArgumentException(
                        sprintf('Language code "%s" does not exist.', $languageCode)
                    );
                }

                return $languageCode;
            }
        );

        $this->output->writeln('');

        $availableGroups = array_keys($this->getContainer()->getParameter('ezpublish.siteaccess.groups'));
        $availableGroups[] = 'default';

        $siteAccessGroup = $this->askForChoiceData(
            'site-access-group',
            'Enter the siteaccess group name on which the Netgen Admin UI configuration will be based. This is usually the name of your frontend siteaccess group',
            $availableGroups,
            current($availableGroups)
        );

        $this->writeSection('Summary before installation');

        $this->output->writeln(
            array(
                'You are going to generate legacy <info>' . $siteAccess . '</info> siteaccess with <info>' . $languageCode . '</info> language code based on <info>' . $siteAccessGroup . '</info> siteaccess group.',
                '',
            )
        );

        if (
            !$this->questionHelper->ask(
                $this->input,
                $this->output,
                $this->getConfirmationQuestion(
                    'Do you confirm installation (answering <comment>no</comment> will restart the process)',
                    true
                )
            )
        ) {
            $this->output->writeln('');

            return false;
        }

        return true;
    }

    /**
     * Runs the command.
     *
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     *
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (!$input->isInteractive()) {
            $output->writeln('<error>This command only supports interactive execution</error>');

            return 1;
        }

        $this->writeSection('Installation');

        // Generate legacy siteaccess
        $legacySiteAccessGenerator = new LegacySiteAccessGenerator(
            $this->getContainer(),
            $this->questionHelper
        );

        $legacySiteAccessGenerator->generate($this->input, $this->output);

        // Generate configuration
        $configurationGenerator = new ConfigurationGenerator(
            $this->getContainer(),
            $this->questionHelper
        );

        $configurationGenerator->generate($this->input, $this->output);

        $errors = array();
        $runner = $this->getRunner($errors);

        // Generate legacy autoloads
        $runner($this->generateLegacyAutoloads());

        $this->writeInstallerSummary($errors);

        return 0;
    }

    /**
     * Generates legacy autoloads.
     *
     * @return array
     */
    protected function generateLegacyAutoloads()
    {
        $this->output->writeln('');
        $this->output->write('Generating legacy autoloads... ');

        $currentWorkingDirectory = getcwd();

        try {
            chdir($this->getContainer()->getParameter('ezpublish_legacy.root_dir'));

            $processBuilder = new ProcessBuilder(
                array(
                    'php',
                    'bin/php/ezpgenerateautoloads.php',
                    '--quiet',
                )
            );

            $process = $processBuilder->getProcess();

            $process->setTimeout(3600);
            $process->run(
                function ($type, $buffer) {
                    echo $buffer;
                }
            );

            chdir($currentWorkingDirectory);

            if (!$process->isSuccessful()) {
                return array(
                    '- Run the following command from your ezpublish_legacy root to generate legacy autoloads:',
                    '',
                    '    <comment>php bin/php/ezpgenerateautoloads.php</comment>',
                    '',
                );
            }
        } catch (Exception $e) {
            chdir($currentWorkingDirectory);

            return array(
                'There was an error generating legacy autoloads: ' . $e->getMessage(),
                '',
            );
        }
    }

    /**
     * Asks a question that fills provided option.
     *
     * @param string $optionIdentifier
     * @param string $optionName
     * @param string $defaultValue
     * @param callable $validator
     *
     * @return string
     */
    protected function askForData($optionIdentifier, $optionName, $defaultValue, $validator = null)
    {
        $optionValue = $this->input->getOption($optionIdentifier);
        $optionValue = !empty($optionValue) ? $optionValue :
            $defaultValue;

        $question = $this->getQuestion($optionName, $optionValue, $validator);
        $optionValue = $this->questionHelper->ask(
            $this->input,
            $this->output,
            $question
        );

        $this->input->setOption($optionIdentifier, $optionValue);

        return $optionValue;
    }

    /**
     * Asks a choice question that fills provided option.
     *
     * @param string $optionIdentifier
     * @param string $optionName
     * @param array $choices
     * @param string $defaultValue
     *
     * @return string
     */
    protected function askForChoiceData($optionIdentifier, $optionName, array $choices, $defaultValue)
    {
        $optionValue = $this->input->getOption($optionIdentifier);
        $optionValue = !empty($optionValue) ? $optionValue :
            $defaultValue;

        $question = $this->getChoiceQuestion($optionName, $optionValue, $choices);
        $optionValue = $this->questionHelper->ask(
            $this->input,
            $this->output,
            $question
        );

        $this->input->setOption($optionIdentifier, $optionValue);

        return $optionValue;
    }

    /**
     * Instantiates and returns a choice question.
     *
     * @param string $questionName
     * @param string $defaultValue
     * @param callable $validator
     *
     * @return \Symfony\Component\Console\Question\Question
     */
    protected function getQuestion($questionName, $defaultValue = null, $validator = null)
    {
        $questionName = $defaultValue
            ? '<info>' . $questionName . '</info> [<comment>' . $defaultValue . '</comment>]: '
            : '<info>' . $questionName . '</info>: ';

        $question = new Question($questionName, $defaultValue);
        if ($validator !== null) {
            $question->setValidator($validator);
        }

        return $question;
    }

    /**
     * Instantiates and returns a question.
     *
     * @param string $questionName
     * @param string $defaultValue
     * @param array $choices
     *
     * @return \Symfony\Component\Console\Question\Question
     */
    protected function getChoiceQuestion($questionName, $defaultValue = null, array $choices = array())
    {
        $questionName = $defaultValue
            ? '<info>' . $questionName . '</info> [<comment>' . $defaultValue . '</comment>]: '
            : '<info>' . $questionName . '</info>: ';

        return new ChoiceQuestion($questionName, $choices, $defaultValue);
    }

    /**
     * Instantiates and returns the confirmation question.
     *
     * @param string $questionName
     * @param bool $defaultValue
     *
     * @return \Symfony\Component\Console\Question\ConfirmationQuestion
     */
    protected function getConfirmationQuestion($questionName, $defaultValue = false)
    {
        return new ConfirmationQuestion(
            sprintf(
                '<info>%s</info> [<comment>%s</comment>]? ',
                $questionName,
                $defaultValue ? 'yes' : 'no'
            ),
            $defaultValue
        );
    }

    /**
     * Writes installer summary.
     *
     * @param array $errors
     */
    protected function writeInstallerSummary($errors)
    {
        if (!$errors) {
            $this->writeSection('You can now continue installation as per instructions in the README.md file!');

            return;
        }

        $this->writeSection(
            array(
                'The command was not able to install everything automatically.',
                'You must do the following changes manually.',
            ),
            'error'
        );

        $this->output->writeln($errors);
    }

    /**
     * Writes a section of text to the output.
     *
     * @param string|array $text
     * @param string $style
     */
    protected function writeSection($text, $style = 'bg=blue;fg=white')
    {
        $this->output->writeln(
            array(
                '',
                $this->getHelper('formatter')->formatBlock($text, $style, true),
                '',
            )
        );
    }

    /**
     * Returns the runner.
     *
     * @param array $errors
     *
     * @return callable
     */
    protected function getRunner(&$errors)
    {
        $output = $this->output;
        $runner = function ($err) use ($output, &$errors) {
            if (!empty($err)) {
                $output->writeln('<fg=red>FAILED</>');
                $errors = array_merge($errors, $err);
            } else {
                $output->writeln('<info>OK</info>');
            }
        };

        return $runner;
    }
}


<?php

namespace Qless;

use Qless\Exceptions\QlessException;
use Qless\Jobs\Collection as JobsCollection;
use Qless\Queues\Collection as QueuesCollection;
use Qless\Subscribers\WatchdogSubscriber;
use Qless\Support\PropertyAccessor;
use Qless\Workers\Collection as WorkersCollection;
use Predis\Client as Redis;

/**
 * Qless\Client
 *
 * Client to call lua scripts in qless-core for specific commands.
 *
 * @package Qless
 *
 * @method string put(string $worker, string $queue, string $jid, string $klass, string $data, int $delay, ...$args)
 * @method string recur(string $queue, string $jid, string $klass, string $data, string $spec, ...$args)
 * @method string requeue(string $worker, string $queue, string $jid, string $klass, string $data, int $delay, ...$args)
 * @method string pop(string $queue, string $worker, int $count)
 * @method string popByJid(string $queue, string $jid, string $worker)
 * @method int length(string $queue)
 * @method float heartbeat(...$args)
 * @method int retry(string $jid, string $queue, string $worker, int $delay, string $group, string $message)
 * @method array cancel(string $jid)
 * @method int unrecur(string $jid)
 * @method bool|string fail(string $jid, string $worker, string $group, string $message, ?string $data = null)
 * @method string[] jobs(string $state, string $queue, int $offset = 0, int $count = 25)
 * @method null|string get(string $jid)
 * @method string multiget(string[] $jid)
 * @method string complete(string $jid, string $workerName, string $queueName, string $data, ...$args)
 * @method void timeout(string $jid)
 * @method string failed(string|bool $group = false, int $start = 0, int $limit = 25)
 * @method string tracked()
 * @method string[] tag(string $op, $tags)
 * @method string stats(string $queueName, int $date)
 * @method void pause(string $queueName, ...$args)
 * @method string queues(?string $queueName = null)
 * @method void unpause(string $queueName, ...$args)
 * @method string workers(?string $workerName = null)
 * @method string workerJobs(string $worker, ?int $minScoreTime = null)
 * @method string subscription(string $queue, $op, $topic)
 *
 * @property-read JobsCollection $jobs
 * @property-read WorkersCollection $workers
 * @property-read QueuesCollection $queues
 * @property-read Config $config
 * @property-read LuaScript $lua
 */
class Client implements EventsManagerAwareInterface
{
    use EventsManagerAwareTrait, PropertyAccessor;

    /** @var LuaScript */
    private $lua;

    /** @var Config */
    private $config;

    /** @var JobsCollection */
    private $jobs;

    /** @var WorkersCollection */
    private $workers;

    /** @var QueuesCollection */
    private $queues;

    /** @var Redis */
    private $redis;

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

    /**
     * Client constructor.
     *
     * @param  mixed $parameters Connection parameters for one or more servers.
     * @param  mixed $options    Options to configure some behaviours of the client.
     *
     * @throws \Qless\Exceptions\InvalidArgumentException
     * @throws \Qless\Exceptions\RedisConnectionException
     */
    public function __construct($parameters = null, $options = null)
    {
        $this->workerName = php_uname('n') . '-' . getmypid();

        $this->redis = new Redis($parameters, $options);
        $this->redis->connect();

        $this->setEventsManager(new EventsManager());

        $this->lua = new LuaScript($this->redis);
        $this->config = new Config($this);
        $this->jobs = new JobsCollection($this);
        $this->workers = new WorkersCollection($this);
        $this->queues = new QueuesCollection($this);
    }

    /**
     * Gets internal worker name.
     *
     * @return string
     */
    public function getWorkerName(): string
    {
        return $this->workerName;
    }

    /**
     * Factory method to create a new Subscriber instance.
     *
     * NOTE: use separate connections for pub and sub.
     * @link https://stackoverflow.com/questions/22668244/should-i-use-separate-connections-for-pub-and-sub-with-redis
     *
     * @param  array $channels An array of channels to subscribe to.
     * @return WatchdogSubscriber
     *
     * @throws \Qless\Exceptions\RedisConnectionException
     */
    public function createSubscriber(array $channels): WatchdogSubscriber
    {
        $redis = clone $this->redis;

        $redis->disconnect();
        $redis->connect();

        return new WatchdogSubscriber($redis, $channels);
    }

    /**
     * Call a specific q-less command.
     *
     * @param  string $command
     * @param  mixed  ...$arguments
     * @return mixed|null
     *
     * @throws QlessException
     */
    public function call(string $command, ...$arguments)
    {
        $arguments = func_get_args();
        array_shift($arguments);

        return $this->__call($command, $arguments);
    }

    /**
     * Call a specific q-less command.
     *
     * @param  string $command
     * @param  array $arguments
     * @return mixed
     *
     * @throws QlessException
     */
    public function __call(string $command, array $arguments)
    {
        return $this->lua->run($command, $arguments);
    }

    /**
     * Gets the jobs collection.
     *
     * @return JobsCollection
     */
    public function getJobs(): JobsCollection
    {
        return $this->jobs;
    }

    /**
     * Gets the workers collection.
     *
     * @return WorkersCollection
     */
    public function getWorkers(): WorkersCollection
    {
        return $this->workers;
    }

    /**
     * Gets the queues collection.
     *
     * @return QueuesCollection
     */
    public function getQueues(): QueuesCollection
    {
        return $this->queues;
    }

    /**
     * Gets the qless config.
     *
     * @return Config
     */
    public function getConfig(): Config
    {
        return $this->config;
    }

    /**
     * Gets the wrapper to load and execute Lua script for qless-core.
     *
     * @return LuaScript
     */
    public function getLua(): LuaScript
    {
        return $this->lua;
    }

    /**
     * Removes all the entries from the default Redis database.
     *
     * @return void
     */
    public function flush(): void
    {
        $this->redis->flushdb();
    }

    /**
     * Call to reconnect to Redis server.
     *
     * @return void
     */
    public function reconnect(): void
    {
        $this->redis->disconnect();
        $this->redis->connect();
    }
}


<?php

namespace Qless\Jobs\Reservers;

use Qless\Exceptions\InvalidArgumentException;
use Qless\Jobs\BaseJob;
use Qless\Queues\Collection;
use Qless\Queues\Queue;

/**
 * Qless\Jobs\Reservers\RoundRobinReserver
 *
 * @package Qless\Jobs\Reservers
 */
class RoundRobinReserver extends AbstractReserver implements ReserverInterface
{
    /** @var int */
    private $numQueues = 0;

    /** @var int  */
    private $lastIndex = 0;

    /**
     * {@inheritdoc}
     *
     * @var string
     */
    const TYPE_DESCRIPTION = 'round robin';

    /**
     * Instantiate a new reserver, given a list of queues that it should be working on.
     *
     * @param  Collection  $collection
     * @param  array|null  $queues
     * @param  string|null $spec
     * @param  string|null $worker
     *
     * @throws InvalidArgumentException
     */
    public function __construct(
        Collection $collection,
        ?array $queues = null,
        ?string $spec = null,
        ?string $worker = null
    ) {
        parent::__construct($collection, $queues, $spec, $worker);

        $this->numQueues = count($this->queues);
        $this->lastIndex = $this->numQueues - 1;
    }

    /**
     * {@inheritdoc}
     *
     * @return BaseJob|null
     */
    final public function reserve(): ?BaseJob
    {
        $this->beforeWork();

        $this->logger->debug('Attempting to reserve a job using {reserver} reserver', [
            'reserver' => $this->getDescription(),
        ]);

        for ($i = 0; $i < $this->numQueues; ++$i) {
            $queue = $this->nextQueue();

            /** @var \Qless\Jobs\BaseJob|null $job */
            $job = $queue->pop($this->worker);
            if ($job !== null) {
                $this->logger->info('Found a job on {queue}', ['queue' => (string) $queue]);
                return $job;
            }
        }

        return null;
    }

    private function nextQueue(): Queue
    {
        $this->lastIndex = ($this->lastIndex + 1) % $this->numQueues;

        return $this->queues[$this->lastIndex];
    }
}


<?php

namespace Qless\Exceptions;

/**
 * Qless\Exceptions\ExceptionFactory
 *
 * @package Qless\Exceptions
 */
class ExceptionFactory
{
    const ERROR_MESSAGE_RE = '/^ERR.*user_script:\d+:\s*(?<area>[\w.]+)\(\):\s*(?<message>.*)/';

    /**
     * Factory method to create an exception instance from an error message.
     *
     * @param  string $error
     * @return InvalidJobException|JobLostException|QlessException
     */
    public static function fromErrorMessage(string $error): QlessException
    {
        $area = null;
        $message = $error;

        if (preg_match(self::ERROR_MESSAGE_RE, $error, $matches) > 0) {
            $area    = $matches['area'];
            $message = $matches['message'];
        }

        switch (true) {
            case ($area === 'Requeue' && stripos($message, 'does not exist') !== false):
            case (stripos($message, 'Job does not exist') !== false):
                return new InvalidJobException($message, $area);
            case (stripos($message, 'Job given out to another worker') !== false):
                return new JobLostException($message, $area);
            case (stripos($message, 'Job not currently running') !== false):
            default:
                return new QlessException($message, $area);
        }
    }
}


<?php

namespace Qless;

use Qless\Exceptions\QlessException;

/**
 * Qless\Config
 *
 * @package Qless
 */
class Config
{
    /** @var Client */
    private $client;

    /**
     * Config constructor.
     *
     * @param Client $client
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    /**
     * Gets the value for the specified config name, falling back to the default if it does not exist.
     *
     * @param  string $name
     * @param  mixed $default
     * @return mixed|null
     *
     * @throws QlessException
     */
    public function get(string $name, $default = null)
    {
        $res = $this->client->call('config.get', $name);

        return $res === null ? $default : $res;
    }

    /**
     * Sets the config name to the specified value.
     *
     * @param  string          $name
     * @param  string|int|bool $value
     * @return void
     *
     * @throws QlessException
     */
    public function set(string $name, $value): void
    {
        $this->client->call('config.set', $name, $value);
    }

    /**
     * Clears the specified config name.
     *
     * @param string $name
     * @return void
     *
     * @throws QlessException
     */
    public function clear($name): void
    {
        $this->client->call('config.unset', $name);
    }
}


<?php

namespace Qless\Jobs;

use Qless\EventsManagerAwareInterface;
use Qless\EventsManagerAwareTrait;
use Qless\Exceptions\InvalidArgumentException;

/**
 * Qless\Jobs\JobFactory
 *
 * @package Qless\Jobs
 */
final class JobFactory implements EventsManagerAwareInterface
{
    use EventsManagerAwareTrait;

    /**
     * Creates the custom job instance.
     *
     * @param  string $className
     * @param  string $performMethod
     * @return mixed
     *
     * @throws InvalidArgumentException
     */
    public function create(string $className, string $performMethod = 'perform')
    {
        if (class_exists($className) == false) {
            throw new InvalidArgumentException("Could not find job class {$className}.");
        }

        if (method_exists($className, $performMethod) == false) {
            throw new InvalidArgumentException(
                sprintf(
                    'Job class "%s" does not contain perform method "%s".',
                    $className,
                    $performMethod
                )
            );
        }

        $instance = new $className;

        if ($instance instanceof EventsManagerAwareInterface) {
            $instance->setEventsManager($this->getEventsManager());
        }

        return $instance;
    }
}


<?php

namespace Qless\Signals;

use Seld\Signal\SignalHandler as BaseHandler;

/**
 * Qless\Signals\SignalHandler
 *
 * @package Qless\Signals
 */
class SignalHandler extends BaseHandler
{
    const KNOWN_SIGNALS = [
        SIGTERM,
        SIGINT,
        SIGQUIT,
        SIGUSR1,
        SIGUSR2,
        SIGCONT,
    ];

    /**
     * Clear all previously registered signal handlers.
     *
     * @param  string[]|int[]|null $signals
     * @return void
     */
    public static function unregister(?array $signals = null): void
    {
        if (empty($signals)) {
            $signals = self::KNOWN_SIGNALS;
        }

        foreach ($signals as $signal) {
            if (is_string($signal)) {
                // skip missing signals, for example OSX does not have all signals
                if (!defined($signal)) {
                    continue;
                }

                $signal = constant($signal);
            }

            pcntl_signal($signal, SIG_DFL);
        }
    }

    /**
     * Creates a human readable pcntl signal name by its code.
     *
     * @param  int $signal
     * @return string
     */
    public static function sigName(int $signal): string
    {
        $signals = [
            'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGIOT', 'SIGBUS',
            'SIGFPE', 'SIGKILL', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGPIPE', 'SIGALRM', 'SIGTERM',
            'SIGSTKFLT', 'SIGCLD', 'SIGCHLD', 'SIGCONT', 'SIGSTOP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU',
            'SIGURG', 'SIGXCPU', 'SIGXFSZ', 'SIGVTALRM', 'SIGPROF', 'SIGWINCH', 'SIGPOLL', 'SIGIO',
            'SIGPWR', 'SIGSYS', 'SIGBABY',
        ];

        foreach ($signals as $name) {
            if (defined($name) && constant($name) === $signal) {
                return $name;
            }
        }

        return 'UNKNOWN';
    }
}


<?php

namespace Qless\Jobs\Reservers;

/**
 * Qless\Jobs\Reservers\OrderedReserver
 *
 * @package Qless\Jobs\Reservers
 */
class OrderedReserver extends AbstractReserver implements ReserverInterface
{
    /**
     * {@inheritdoc}
     *
     * @var string
     */
    const TYPE_DESCRIPTION = 'ordered';

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function beforeWork(): void
    {
        parent::beforeWork();

        sort($this->queues, SORT_NATURAL);

        $this->resetDescription();

        if (empty($this->queues) == false) {
            $this->logger->info(
                'Monitoring the following queues: {queues}',
                ['queues' => implode(', ', $this->queues)]
            );
        }
    }
}


<?php

namespace Qless\Jobs\Reservers;

/**
 * Qless\Jobs\Reservers\ShuffledRoundRobinReserver
 *
 * @package Qless\Jobs\Reservers
 */
class ShuffledRoundRobinReserver extends RoundRobinReserver
{
    /**
     * {@inheritdoc}
     *
     * @var string
     */
    const TYPE_DESCRIPTION = 'shuffled round robin';

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function beforeWork(): void
    {
        parent::beforeWork();

        shuffle($this->queues);

        if (empty($this->queues) == false) {
            $this->logger->info(
                'Monitoring the following queues: {queues}',
                ['queues' => implode(', ', $this->queues)]
            );
        }
    }
}


<?php

namespace Qless\Queues;

use ArrayAccess;
use Qless\Client;
use Qless\Exceptions\UnsupportedFeatureException;
use Qless\Support\PropertyAccessor;

/**
 * Qless\Queues\Collection
 *
 * @property-read array $counts
 *
 * @package Qless\Queues
 */
class Collection implements ArrayAccess
{
    use PropertyAccessor;

    /** @var Client */
    private $client;

    /**
     * Collection constructor.
     *
     * @param Client $client
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    /**
     * What queues are there, and how many jobs do they have running, waiting, scheduled, etc.
     *
     * @return array
     */
    public function getCounts(): array
    {
        return json_decode($this->client->queues(), true) ?: [];
    }

    /**
     * Gets a list of existent Queues matched by specification (regular expression).
     *
     * @param  string $regexp
     * @return Queue[]
     */
    public function fromSpec(string $regexp): array
    {
        $response = [];

        if (empty($regexp)) {
            return $response;
        }

        $queues = json_decode($this->client->queues(), true) ?: [];

        foreach ($queues as $queue) {
            if (isset($queue['name']) && preg_match("/^$regexp$/", $queue['name'])) {
                $response[] = new Queue($queue['name'], $this->client);
            }
        }

        return $response;
    }

    /**
     * Gets a list of existent Queues matched by topic.
     *
     * @param  string $topic
     * @return Queue[]
     */
    public function fromSubscriptions(string $topic): array
    {
        $response = [];

        if (empty($topic)) {
            return $response;
        }

        $subscriptions = $this->client->call('subscription', 'default', 'all', $topic);
        $subscriptions = json_decode($subscriptions, true) ?: [];

        foreach ($subscriptions as $subscription => $queues) {
            $topicPattern = str_replace(['.', '*', '#'], ['\.', '[a-zA-z0-9^.]{1,}', '.*'], $subscription);
            if (preg_match("/^$topicPattern$/", $topic)) {
                $response = array_merge($response, $queues);
            }
        }

        return array_unique($response);
    }

    /**
     * {@inheritdoc}
     *
     * @param  mixed $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        $queues = json_decode($this->client->queues(), true) ?: [];

        foreach ($queues as $queue) {
            if (isset($queue['name']) && $queue['name'] === $offset) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get a queue object associated with the provided queue name.
     *
     * @param  string $offset
     * @return Queue
     */
    public function offsetGet($offset)
    {
        return new Queue($offset, $this->client);
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetSet($offset, $value)
    {
        throw new UnsupportedFeatureException('Setting a queue is not supported using Queues collection.');
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetUnset($offset)
    {
        throw new UnsupportedFeatureException('Deleting a queue is not supported using Queues collection.');
    }
}


<?php

namespace Qless\Workers;

use ArrayAccess;
use Qless\Client;
use Qless\Exceptions\UnsupportedFeatureException;
use Qless\Support\PropertyAccessor;

/**
 * Qless\Workers\Collection
 *
 * Collection for accessing worker information lazily.
 *
 * @property-read array $counts
 *
 * @package Qless\Workers
 */
class Collection implements ArrayAccess
{
    use PropertyAccessor;

    /** @var Client */
    private $client;

    /**
     * Collection constructor.
     *
     * @param Client $client
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    /**
     * What workers are workers, and how many jobs are they running.
     *
     * @return array
     */
    public function getCounts(): array
    {
        return json_decode($this->client->workers(), true) ?: [];
    }

    /**
     * {@inheritdoc}
     *
     * @param  mixed $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        $workers = json_decode($this->client->workers(), true) ?: [];

        foreach ($workers as $worker) {
            if (isset($worker['name']) && $worker['name'] === $offset) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     *
     * @param  string $offset
     * @return array
     */
    public function offsetGet($offset)
    {
        $worker = json_decode($this->client->workers($offset), true) ?: [];

        $worker['jobs'] = $worker['jobs'] ?? [];
        $worker['stalled'] = $worker['stalled'] ?? [];

        return $worker;
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetSet($offset, $value)
    {
        throw new UnsupportedFeatureException('Setting a worker is not supported using Workers collection.');
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetUnset($offset)
    {
        throw new UnsupportedFeatureException('Deleting a worker is not supported using Workers collection.');
    }

    /**
     * @param string $worker
     *
     * @return bool
     */
    public function remove(string $worker): bool
    {
        if ($this->offsetExists($worker)) {
            return $this->client->call('worker.deregister', $worker) === 1;
        }

        return false;
    }
}


<?php

namespace Qless\Jobs;

use ArrayAccess;
use Qless\Client;
use Qless\Exceptions\QlessException;
use Qless\Exceptions\UnsupportedFeatureException;

/**
 * Qless\Jobs\Collection
 *
 * A class for interacting with jobs.
 * Not meant to be instantiated directly, it's accessed through {@see Client::$jobs}.
 *
 * @package Qless\Jobs
 */
class Collection implements ArrayAccess
{
    /** @var Client */
    private $client;

    /**
     * Collection constructor.
     *
     * @param Client $client
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    /**
     * Return a paginated list of JIDs which are in a completed state
     *
     * @param  int $offset
     * @param  int $count
     * @return string[]
     */
    public function completed(int $offset = 0, int $count = 25)
    {
        return $this->client->jobs('complete', null, $offset, $count);
    }

    /**
     * Return either a {@see BaseJob} or a {@see RecurringJob} instance for the
     * specified job identifier. Otherwise return NULL if the job does not exist.
     *
     * @param  string $jid the job identifier to fetch
     * @return BaseJob|RecurringJob|null
     *
     * @throws QlessException
     */
    public function get(string $jid)
    {
        return $this->offsetGet($jid);
    }

    /**
     * Returns an array of jobs for the specified job identifiers, keyed by job identifier
     *
     * @param  string[] $jids
     * @return BaseJob[]
     */
    public function multiget(array $jids): array
    {
        if (empty($jids)) {
            return [];
        }

        $results = call_user_func_array([$this->client, 'multiget'], $jids);
        $jobs = json_decode($results, true) ?: [];

        $ret = [];
        foreach ($jobs as $data) {
            $job = new BaseJob($this->client, $data);
            $job->setEventsManager($this->client->getEventsManager());

            $ret[$job->jid] = $job;
        }

        return $ret;
    }

    /**
     * Fetches a report of failed jobs for the specified group
     *
     * @param string|bool $group
     * @param int         $start
     * @param int         $limit
     *
     * @return array
     */
    public function failedForGroup($group, int $start = 0, int $limit = 25): array
    {
        $results = json_decode($this->client->failed($group, $start, $limit), true);

        if (isset($results['jobs']) && !empty($results['jobs'])) {
            $results['jobs'] = $this->multiget($results['jobs']);
        }

        return is_array($results) ? $results : [];
    }

    /**
     * Fetches a report of failed jobs, where the key is the group and the value is the number of jobs
     *
     * @return array
     */
    public function failed(): array
    {
        $results = json_decode($this->client->failed(), true);

        return is_array($results) ? $results : [];
    }

    /**
     * Fetches a list of tagged job ids associated with provided tag.
     *
     * @param  string $tag
     * @param  int    $offset
     * @param  int    $limit
     * @return string[]
     */
    public function tagged(string $tag, int $offset = 0, int $limit = 25): array
    {
        $response = json_decode($this->client->call('tag', 'get', $tag, $offset, $limit), true);
        if (empty($response['jobs'])) {
            $response['jobs'] = [];
        }

        return $response['jobs'];
    }

    /**
     * Fetches a list of tracked jobs.
     *
     * @return array
     */
    public function tracked(): array
    {
        $tracked = $this->client->call('tracked') ?? '[]';

        return json_decode($tracked, true);
    }

    /**
     * Reads jobs in a worker.
     *
     * @param string $worker
     * @param string $subTimeInterval  specify last time interval, i.e. '2 hours', '15 mins' and etc; all jobs on empty
     * @return array  BaseJob[]
     */
    public function fromWorker(string $worker, string $subTimeInterval = ''): array
    {
        try {
            $now = new \DateTime();
            $interval = date_interval_create_from_date_string($subTimeInterval);
            $timestamp = $now->sub($interval)->getTimestamp();
        } catch (\Exception $e) {
            $timestamp = -1;
        }

        if ($subTimeInterval === '' || $timestamp === -1) {
            $jids = json_decode($this->client->workerJobs($worker), true) ?: [];
        } else {
            $jids = json_decode($this->client->workerJobs($worker, $timestamp), true) ?: [];
        }

        return $this->multiget($jids);
    }

    /**
     * {@inheritdoc}
     *
     * @param  string $jid
     * @return bool
     */
    public function offsetExists($jid)
    {
        return $this->client->get($jid) !== null;
    }

    /**
     * {@inheritdoc}
     *
     * @param  string $jid
     * @return BaseJob|RecurringJob|null
     *
     * @throws QlessException
     */
    public function offsetGet($jid)
    {
        $data = $this->client->get($jid);

        if (empty($data)) {
            $data = $this->client->call('recur.get', $jid);
            if (empty($data)) {
                return null;
            }

            $job = new RecurringJob($this->client, json_decode($data, true));
        } else {
            $job = new BaseJob($this->client, json_decode($data, true));
        }

        $job->setEventsManager($this->client->getEventsManager());

        return $job;
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetSet($offset, $value)
    {
        throw new UnsupportedFeatureException('Setting a job is not supported using Jobs collection.');
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnsupportedFeatureException
     */
    public function offsetUnset($offset)
    {
        throw new UnsupportedFeatureException('Deleting a job is not supported using Jobs collection.');
    }
}


<?php

namespace Qless\Workers;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Qless\Client;
use Qless\Events\User\Worker as WorkerEvent;
use Qless\EventsManagerAwareInterface;
use Qless\EventsManagerAwareTrait;
use Qless\Jobs\BaseJob;
use Qless\Jobs\PerformAwareInterface;
use Qless\Jobs\Reservers\ReserverInterface;

/**
 * Qless\Workers\AbstractWorker
 *
 * @package Qless\Workers
 */
abstract class AbstractWorker implements WorkerInterface, EventsManagerAwareInterface
{
    use EventsManagerAwareTrait;

    /**
     * The interval for checking for new jobs.
     *
     * @var int
     */
    protected $interval = 5;

    /**
     * The internal client to communicate with qless-core.
     *
     * @var Client
     */
    protected $client;

    /**
     * The job reserver.
     *
     * @var ReserverInterface
     */
    protected $reserver;

    /**
     * Logging object that implements the PSR-3 LoggerInterface.
     *
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * String identifying this worker.
     *
     * @var string
     */
    protected $name;

    /** @var PerformAwareInterface|null */
    protected $jobPerformHandler;

    /**
     * Current job instance (if is set).
     *
     * @var BaseJob|null
     */
    protected $job;

    /**
     * Is current iteration paused?
     *
     * @var bool
     */
    protected $paused = false;

    /**
     * Is the worker has been halted.
     *
     * @var bool
     */
    private $shutdown = false;

    /**
     * Instantiate a new worker, given a list of queues that it should be working on.
     *
     *
     * @param ReserverInterface $reserver
     * @param Client            $client
     */
    final public function __construct(ReserverInterface $reserver, Client $client)
    {
        $this->reserver = $reserver;
        $this->logger = new NullLogger();
        $this->client = $client;
        $this->name = array_values(array_slice(explode('\\', get_class($this)), -1))[0];

        $this->setEventsManager($client->getEventsManager());

        $this->onConstruct();
    }

    /**
     * Sets current job.
     *
     * @param  BaseJob|null $job
     * @return void
     */
    final public function setCurrentJob(BaseJob $job = null): void
    {
        $this->job = $job;
    }

    /**
     * Make a worker as a halted.
     *
     * @return void
     */
    protected function doShutdown(): void
    {
        $this->shutdown = true;
    }

    /**
     * If the worker has been halted.
     *
     * @return bool
     */
    protected function isShuttingDown(): bool
    {
        return $this->shutdown;
    }

    /**
     * On construct internal event.
     *
     * @return void
     */
    public function onConstruct(): void
    {
    }

    /**
     * {@inheritdoc}
     *
     * @param  int $interval
     * @return void
     */
    final public function setInterval(int $interval): void
    {
        $this->interval = abs($interval);
    }

    /**
     * {@inheritdoc}
     *
     * @param  LoggerInterface $logger
     * @return void
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->reserver->setLogger($logger);
    }

    /**
     * {@inheritdoc}
     *
     * @param  string $name
     * @return void
     */
    final public function setName(string $name): void
    {
        $this->name = $name;
    }

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

    /**
     * @param  PerformAwareInterface $jobHandler Instance of handler
     * @return void
     */
    final public function registerJobPerformHandler(PerformAwareInterface $jobHandler): void
    {
        $this->jobPerformHandler = $jobHandler;
        if ($this->jobPerformHandler instanceof EventsManagerAwareInterface) {
            $this->jobPerformHandler->setEventsManager($this->client->getEventsManager());
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return null|BaseJob
     */
    final public function reserve(): ?BaseJob
    {
        return $this->reserver->reserve();
    }

    /**
     * {@inheritdoc}
     *
     * @link   http://php.net/manual/en/function.setproctitle.php
     *
     * @param  string $value
     * @param  array $context
     * @return void
     */
    final public function title(string $value, array $context = []): void
    {
        $this->logger->info($value, $context);

        $line = sprintf('Qless PHP: %s', $value);

        if (function_exists('setproctitle')) {
            \setproctitle($line);
            return;
        }

        if (@cli_set_process_title($line) === false) {
            if ('Darwin' === PHP_OS) {
                trigger_error(
                    'Running "cli_set_process_title" as an unprivileged user is not supported on macOS.',
                    E_USER_WARNING
                );
            } else {
                $error = error_get_last();
                trigger_error($error['message'], E_USER_WARNING);
            }
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    final public function run(): void
    {
        $this->getEventsManager()->fire(new WorkerEvent\BeforeFirstWork($this));

        $this->perform();
    }

    abstract public function perform(): void;

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function shutdown(): void
    {
        $this->logger->notice('{type}: QUIT received; shutting down', ['type' => $this->name]);

        $this->doShutdown();
    }

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function pauseProcessing(): void
    {
        $this->logger->notice('{type}: USR2 received; pausing job processing', ['type' => $this->name]);
        $this->paused = true;
    }

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function unPauseProcessing(): void
    {
        $this->logger->notice('{type}: CONT received; resuming job processing', ['type' => $this->name]);
        $this->paused = false;
    }

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function shutdownNow(): void
    {
        $this->logger->notice('{type}: Force an immediate shutdown of the worker', ['type' => $this->name]);

        $this->doShutdown();
        $this->killChildren();
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnoreStart
     * @return void
     */
    public function killChildren(): void
    {
        // nothing to do
        return;
    }
    // @codeCoverageIgnoreEnd
}