ManagerWorker   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 61
c 1
b 0
f 0
dl 0
loc 238
ccs 0
cts 54
cp 0
rs 10
wmc 17

6 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveEntityClass() 0 27 3
A __construct() 0 3 1
A loadRepository() 0 25 2
A getRepository() 0 13 3
A resolveRepositoryClass() 0 36 6
A guessEntityClass() 0 17 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Derafu: Biblioteca PHP (Núcleo).
7
 * Copyright (C) Derafu <https://www.derafu.org>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de GNU
20
 * junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace Derafu\Lib\Core\Package\Prime\Component\Entity\Worker;
26
27
use Derafu\Lib\Core\Foundation\Abstract\AbstractWorker;
28
use Derafu\Lib\Core\Package\Prime\Component\Entity\Contract\DatasourceProviderWorkerInterface;
29
use Derafu\Lib\Core\Package\Prime\Component\Entity\Contract\ManagerWorkerInterface;
30
use Derafu\Lib\Core\Package\Prime\Component\Entity\Contract\RepositoryInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Entity\Entity\Entity;
32
use Derafu\Lib\Core\Package\Prime\Component\Entity\Exception\ManagerException;
33
use Derafu\Lib\Core\Package\Prime\Component\Entity\Mapping as DEM;
34
use Derafu\Lib\Core\Package\Prime\Component\Entity\Repository\Repository;
35
use Exception;
36
use ReflectionClass;
37
38
/**
39
 * Worker "prime.entity.manager".
40
 */
41
class ManagerWorker extends AbstractWorker implements ManagerWorkerInterface
42
{
43
    /**
44
     * Sufijo de la interfaz.
45
     *
46
     * @var string
47
     */
48
    private const ENTITY_INTERFACE_SUFFIX = 'Interface';
49
50
    /**
51
     * Namespace de la interfaz.
52
     *
53
     * Importante: solo el nivel inmediatamente superior.
54
     *
55
     * @var string
56
     */
57
    private const ENTITY_INTERFACE_NAMESPACE = 'Contract';
58
59
    /**
60
     * Sufijo de la clase de entidad.
61
     *
62
     * Importante: no se utilizan sufijos en entidades, pero se deja
63
     * estandrizado acá en la constante.
64
     *
65
     * @var string
66
     */
67
    private const ENTITY_CLASS_SUFFIX = ''; // En blanco a propósito.
68
69
    /**
70
     * Namespace de la entidad.
71
     *
72
     * Importante: solo el nivel inmediatamente superior.
73
     *
74
     * @var string
75
     */
76
    private const ENTITY_CLASS_NAMESPACE = 'Entity';
77
78
    /**
79
     * Esquema de configuración del worker.
80
     *
81
     * @var array
82
     */
83
    protected array $configurationSchema = [
84
        'entityClass' => [
85
            'types' => 'string',
86
            'default' => Entity::class,
87
        ],
88
        'repositoryClass' => [
89
            'types' => 'string',
90
            'default' => Repository::class,
91
        ],
92
    ];
93
94
    /**
95
     * Listado de repositorios que ya han sido cargados desde sus orígenes de
96
     * datos.
97
     *
98
     * @var array<string,RepositoryInterface>
99
     */
100
    private array $loaded = [];
101
102
    /**
103
     * Constructor del worker.
104
     *
105
     * @param DatasourceProviderWorkerInterface $datasourceProviderWorker
106
     */
107
    public function __construct(
108
        private DatasourceProviderWorkerInterface $datasourceProviderWorker
109
    ) {
110
    }
111
112
    /**
113
     * {@inheritDoc}
114
     */
115
    public function getRepository(string $repository): RepositoryInterface
116
    {
117
        // Si el repositorio no está cargado se carga.
118
        if (!isset($this->loaded[$repository])) {
119
            try {
120
                $this->loaded[$repository] = $this->loadRepository($repository);
121
            } catch (Exception $e) {
122
                throw new ManagerException($e->getMessage());
123
            }
124
        }
125
126
        // Retornar el repositorio solicitado.
127
        return $this->loaded[$repository];
128
    }
129
130
    /**
131
     * Carga un repositorio con los datos desde un origen de datos.
132
     *
133
     * @param string $repository
134
     * @return RepositoryInterface
135
     */
136
    private function loadRepository(string $repository): RepositoryInterface
137
    {
138
        // Resolver el repositorio que se debe crear.
139
        $entityClass = $this->resolveEntityClass($repository);
140
        $repositoryClass = $this->resolveRepositoryClass($entityClass);
141
142
        // Si el repositorio implementa RepositoryInterface es un repositorio
143
        // con datos que se obtienen desde DatasourceProvider y se deben cargar
144
        // al repositorio (en memoria).
145
        if (in_array(RepositoryInterface::class, class_implements($repositoryClass))) {
146
            $data = $this->datasourceProviderWorker->fetch($repository);
147
            $instance = new $repositoryClass($data, $entityClass);
148
        }
149
150
        // Si el repositorio es otro tipo de clase se instancia sin datos y se
151
        // espera que el repositorio resuelva su carga (ej: desde una base de
152
        // datos).
153
        else {
154
            // TODO: Mejorar retorno de RepositoryInterface con este caso.
155
            $instance = new $repositoryClass();
156
        }
157
158
        // Asignar la instnacia del repositorio como cargada y retornar.
159
        $this->loaded[$repository] =  $instance;
160
        return $this->loaded[$repository];
161
    }
162
163
    /**
164
     * Determina la clase de la entidad que se debe utilizar con el repositorio.
165
     *
166
     * El identificador del repositorio puede ser el FQCN de la clase de la
167
     * entidad (lo ideal) o un identificador genérico que se resolverá a la
168
     * clase de entidad configurada en el worker.
169
     *
170
     * Si el identificador del repositorio es una interfaz se espera:
171
     *
172
     *   - La interfaz esté dentro de un namespace "Contract".
173
     *   - La interfaz tenga como sufijo "Interface".
174
     *   - La entidad esté dentro del namespace "Entity" sin sufijo.
175
     *
176
     * @param string $repository Identificador del repositorio.
177
     * @return string Clase de la entidad para el repositorio.
178
     */
179
    private function resolveEntityClass(string $repository): string
180
    {
181
        // Si el identificador del repositorio "parece" clase se asume que es la
182
        // clase de la entidad o una interfaz de la entidad en el mismo
183
        // namespace.
184
        if (str_contains($repository, '\\')) {
185
            $entityClass = $this->guessEntityClass($repository);
186
        }
187
188
        // Se entrega la clase de la entidad por defecto cuando el identificador
189
        // no es una clase. Si no tiene "\" entonces no tiene namespace, en este
190
        // caso se asume no es una clase.
191
        else {
192
            $entityClass = $this->getConfiguration()->get('entityClass');
193
        }
194
195
        // Lanzar error si la clase no existe pues podría haber sido mal
196
        // escrita por el programador.
197
        if (!class_exists($entityClass)) {
198
            throw new ManagerException(sprintf(
199
                'La clase de entidad %s no existe. ¿Estará mal escrita?',
200
                $entityClass
201
            ));
202
        }
203
204
        // Entregar la clase de la entidad.
205
        return $entityClass;
206
    }
207
208
    /**
209
     * Adivina la clase de entidad en caso que la clase de entrada sea una
210
     * interfaz.
211
     *
212
     * @param string $class
213
     * @return string
214
     */
215
    private function guessEntityClass(string $class): string
216
    {
217
        // Si la clase es una interfaz se asume una clase de entidad en el mismo
218
        // namespace. Esto es rígido y requiere un formato para el FQCN de la
219
        // clase y la interfaz. Por ahora es suficiente.
220
        if (str_ends_with($class, self::ENTITY_INTERFACE_SUFFIX)) {
221
            $length = strlen($class) - strlen(self::ENTITY_INTERFACE_SUFFIX);
222
            return str_replace(
223
                '\\' . self::ENTITY_INTERFACE_NAMESPACE .  '\\',
224
                '\\' . self::ENTITY_CLASS_NAMESPACE  . '\\',
225
                substr($class, 0, $length)
226
            ) . self::ENTITY_CLASS_SUFFIX;
227
        }
228
229
        // Se entrega la misma clase, pues no tiene el formato esperado para
230
        // adiviar la clase de entidad.
231
        return $class;
232
    }
233
234
    /**
235
     * Determina la clase del repositorio que se debe utilizar para una entidad.
236
     *
237
     * Si la clase de la entidad no provee la información de su repositorio
238
     * asociado se entregará la clase de repositorio configurada en el worker.
239
     *
240
     * @param string $entityClass Clase de la entidad.
241
     * @return string Clase del repositorio.
242
     */
243
    private function resolveRepositoryClass(string $entityClass): string
244
    {
245
        // Se trata de obtener la clase del repositorio desde el método estático
246
        // de la entidad getRepositoryClass().
247
        if (method_exists($entityClass, 'getRepositoryClass')) {
248
            $repositoryClass = call_user_func([$entityClass, 'getRepositoryClass']);
249
            if ($repositoryClass) {
250
                return $repositoryClass;
251
            }
252
        }
253
254
        // Se trata de obtener la clase del repositorio desde un atributo PHP de
255
        // la clase de la entidad.
256
        $reflectionClass = new ReflectionClass($entityClass);
257
        $attributes = $reflectionClass->getAttributes(DEM\Entity::class);
258
        if (!empty($attributes)) {
259
            $repositoryClass = $attributes[0]->newInstance()->repositoryClass;
260
            if ($repositoryClass) {
261
                return $repositoryClass;
262
            }
263
        }
264
265
        // Se entrega la clase del repositorio por defecto configurada.
266
        $repositoryClass = $this->getConfiguration()->get('repositoryClass');
267
268
        // Lanzar error si la clase no existe pues podría haber sido mal
269
        // escrita por el programador.
270
        if (!class_exists($repositoryClass)) {
271
            throw new ManagerException(sprintf(
272
                'La clase de repositorio %s no existe. ¿Estará mal escrita?',
273
                $repositoryClass
274
            ));
275
        }
276
277
        // Entregar clase del repositorio.
278
        return $repositoryClass;
279
    }
280
}
281