Classes/Core/Definition/Builder/DefinitionBuilder.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
declare(strict_types=1);

/*
 * Copyright (C)
 * Nathan Boiron <nathan.boiron@gmail.com>
 * Romain Canon <romain.hydrocanon@gmail.com>
 *
 * This file is part of the TYPO3 NotiZ project.
 * It is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License, either
 * version 3 of the License, or any later version.
 *
 * For the full copyright and license information, see:
 * http://www.gnu.org/licenses/gpl-3.0.html
 */

namespace CuyZ\Notiz\Core\Definition\Builder;

use CuyZ\Notiz\Core\Definition\Builder\Component\DefinitionComponents;
use CuyZ\Notiz\Core\Definition\Tree\Definition;
use CuyZ\Notiz\Core\Support\NotizConstants;
use CuyZ\Notiz\Service\CacheService;
use CuyZ\Notiz\Service\Traits\ExtendedSelfInstantiateTrait;
use CuyZ\Notiz\Validation\Validator\DefinitionValidator;
use Romm\ConfigurationObject\ConfigurationObjectFactory;
use Romm\ConfigurationObject\ConfigurationObjectInstance;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;

/**
 * This class is responsible for building a whole PHP definition object that can
 * then be used everywhere in the API.
 *
 * It works with two types of components:
 *
 * Source components
 * -----------------
 *
 * @see \CuyZ\Notiz\Core\Definition\Builder\Component\Source\DefinitionSource
 *
 * They are used to fetch a definition array from any origin, like TypoScript,
 * YAML or others. This array must be a representation of the definition object
 * used in this extension.
 *
 * The final definition array will be a merge of all the results of the source
 * components.
 *
 * Processor components
 * --------------------
 *
 * @see \CuyZ\Notiz\Core\Definition\Builder\Component\Processor\DefinitionProcessor
 *
 * Once the array definition has been calculated by calling all the source
 * components, a definition object is created. This object can be modified after
 * its creation, by adding so-called "processors" to the builder components.
 *
 * These processor components will have access to the definition object, and can
 * basically use any public method available to add/remove/modify any data.
 *
 * ---
 *
 * Register new components
 * -----------------------
 *
 * To register new components in your own API, you first need to connect a class
 * on a signal. Add this code to your `ext_localconf.php` file:
 *
 * ```
 * $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
 *     \TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class
 * );
 *
 * $dispatcher->connect(
 *     \CuyZ\Notiz\Core\Definition\Builder\DefinitionBuilder::class,
 *     \CuyZ\Notiz\Core\Definition\Builder\DefinitionBuilder::COMPONENTS_SIGNAL,
 *     \Vendor\MyExtension\Domain\Definition\Builder\Component\MyCustomComponents::class,
 *     'register'
 * );
 * ```
 *
 * The registration class should then look like this:
 *
 * ```
 * class MyCustomComponents
 * {
 *     public function register(\CuyZ\Notiz\Core\Definition\Builder\Component\DefinitionComponents $components)
 *     {
 *         $components->addSource(
 *             'mySourceIdentifier',
 *             \Vendor\MyExtension\Domain\Definition\Builder\Source\MySource::class
 *         );
 *
 *         $components->addProcessor(
 *             'myProcessorIdentifier',
 *             \Vendor\MyExtension\Domain\Definition\Builder\Processor\MyProcessor::class
 *         );
 *     }
 * }
 * ```
 */
class DefinitionBuilder implements SingletonInterface
{
    use ExtendedSelfInstantiateTrait;

    const COMPONENTS_SIGNAL = 'manageDefinitionComponents';
    const DEFINITION_BUILT_SIGNAL = 'definitionBuilt';

    /**
     * @var DefinitionComponents
     */
    protected $components;

    /**
     * @var ConfigurationObjectInstance
     */
    protected $definitionObject;

    /**
     * @var CacheService
     */
    protected $cacheService;

    /**
     * @var Dispatcher
     */
    protected $dispatcher;

    /**
     * @param DefinitionComponents $components
     * @param CacheService $cacheService
     * @param Dispatcher $dispatcher
     */
    public function __construct(DefinitionComponents $components, CacheService $cacheService, Dispatcher $dispatcher)
    {
        $this->components = $components;
        $this->cacheService = $cacheService;
        $this->dispatcher = $dispatcher;
    }

    /**
     * Builds a complete definition object, using registered sources and
     * processors.
     *
     * If no error occurred during the build, the instance is put in cache so it
     * can be retrieved during future request.
     *
     * @internal do not use in your own API!
     *
     * @return ConfigurationObjectInstance
     */
    public function buildDefinition(): ConfigurationObjectInstance
    {
        if (null === $this->definitionObject) {
            if ($this->cacheService->has(NotizConstants::CACHE_KEY_DEFINITION_OBJECT)) {
                $this->definitionObject = $this->cacheService->get(NotizConstants::CACHE_KEY_DEFINITION_OBJECT);
            }

            if (false === $this->definitionObject instanceof ConfigurationObjectInstance) {
                $this->definitionObject = $this->buildDefinitionInternal();
                $validationResult = $this->definitionObject->getValidationResult();

                if (false === $validationResult->hasErrors()) {
                    /** @var DefinitionValidator $definitionValidator */
                    $definitionValidator = GeneralUtility::makeInstance(DefinitionValidator::class);

                    $result = $definitionValidator->validate($this->definitionObject->getObject());

                    if ($result->hasErrors()) {
                        $validationResult->merge($result);
                    } else {
                        $this->cacheService->set(NotizConstants::CACHE_KEY_DEFINITION_OBJECT, $this->definitionObject);
                    }
                }
            }

            $this->sendDefinitionBuiltSignal();
        }

        return $this->definitionObject;
    }

    /**
     * Runs the registered source components to get a definition array, then use
     * this array to create a definition object.
     *
     * This object is then passed to each registered processor component, that
     * is used to modify the object data.
     *
     * @return ConfigurationObjectInstance
     */
    protected function buildDefinitionInternal(): ConfigurationObjectInstance
    {
        $arrayDefinition = [];

        $this->sendComponentsSignal();

        foreach ($this->components->getSources() as $source) {
            ArrayUtility::mergeRecursiveWithOverrule($arrayDefinition, $source->getDefinitionArray());
        }

        $definitionObject = ConfigurationObjectFactory::convert(Definition::class, $arrayDefinition);

        $this->runProcessors($definitionObject);

        return $definitionObject;
    }

    /**
     * Runs the registered processors, by giving them the previously created
     * definition object that they can modify like they need to.
     *
     * @param ConfigurationObjectInstance $definitionObject
     */
    protected function runProcessors(ConfigurationObjectInstance $definitionObject)
    {
        if (false === $definitionObject->getValidationResult()->hasErrors()) {
            /** @var Definition $definition */
            $definition = $definitionObject->getObject();

            foreach ($this->components->getProcessors() as $processor) {
                $processor->process($definition);
            }
        }
    }

    /**
     * Sends a signal to allow external API to manage their own definition
     * components.
     */
    protected function sendComponentsSignal()
    {
        $this->dispatcher->dispatch(
            self::class,
            self::COMPONENTS_SIGNAL,
            [$this->components]
        );
    }

    /**
     * Sends a signal when the definition object is complete.
     *
     * Please be aware that this signal is sent only if no error was found when
     * the definition was built.
     */
    protected function sendDefinitionBuiltSignal()
    {
        if (!$this->definitionObject->getValidationResult()->hasErrors()) {
            $this->dispatcher->dispatch(
                self::class,
                self::DEFINITION_BUILT_SIGNAL,
                [$this->definitionObject->getObject()]
            );
        }
    }
}