Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.11% covered (success)
90.11%
82 / 91
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieRouteLoader
90.11% covered (success)
90.11%
82 / 91
66.67% covered (warning)
66.67%
2 / 3
22.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load
89.89% covered (warning)
89.89%
80 / 89
0.00% covered (danger)
0.00%
0 / 1
20.41
 supports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\ApieBundle\Routing;
3
4use Apie\Cms\RouteDefinitions\CmsRouteDefinitionProvider;
5use Apie\Common\Interfaces\GlobalRouteDefinitionProviderInterface;
6use Apie\Common\Interfaces\HasRouteDefinition;
7use Apie\Common\Interfaces\RouteDefinitionProviderInterface;
8use Apie\Common\Lists\UrlPrefixList;
9use Apie\Common\RouteDefinitions\ActionHashmap;
10use Apie\Common\RouteDefinitions\PossibleRoutePrefixProvider;
11use Apie\Core\ApieLib;
12use Apie\Core\Attributes\Route as AttributesRoute;
13use Apie\Core\BoundedContext\BoundedContext;
14use Apie\Core\BoundedContext\BoundedContextHashmap;
15use Apie\Core\BoundedContext\BoundedContextId;
16use Apie\Core\ContextBuilders\ContextBuilderFactory;
17use Apie\Core\ContextConstants;
18use Apie\Core\Enums\RequestMethod;
19use Apie\Core\ValueObjects\UrlRouteDefinition;
20use Apie\RestApi\RouteDefinitions\RestApiRouteDefinitionProvider;
21use Psr\Log\LoggerInterface;
22use Psr\Log\NullLogger;
23use ReflectionClass;
24use Symfony\Component\Config\Loader\Loader;
25use Symfony\Component\Config\Resource\DirectoryResource;
26use Symfony\Component\Config\Resource\FileExistenceResource;
27use Symfony\Component\Config\Resource\GlobResource;
28use Symfony\Component\Config\Resource\ReflectionClassResource;
29use Symfony\Component\Routing\Route;
30use Symfony\Component\Routing\RouteCollection;
31
32/**
33 * Loads the Apie routing into the symfony loader system.
34 */
35final class ApieRouteLoader extends Loader
36{
37    private bool $loaded = false;
38
39    /**
40     * @param array<string, string> $scanBoundedContexts
41     */
42    public function __construct(
43        private readonly RouteDefinitionProviderInterface $routeProvider,
44        private readonly BoundedContextHashmap $boundedContextHashmap,
45        private readonly PossibleRoutePrefixProvider $routePrefixProvider,
46        private readonly ContextBuilderFactory $contextBuilder,
47        private readonly LoggerInterface $logger = new NullLogger(),
48        private readonly array $scanBoundedContexts = [],
49    ) {
50    }
51
52    /**
53     * @param mixed $resource
54     * @param mixed $type
55     */
56    public function load($resource, $type = null): RouteCollection
57    {
58        if (true === $this->loaded) {
59            throw new \RuntimeException('Do not add the "apie" loader twice');
60        }
61
62        $routes = new RouteCollection();
63        $classesForCaching = [
64            __CLASS__,
65            $this->routeProvider,
66            $this->boundedContextHashmap,
67            $this->routePrefixProvider,
68            RestApiRouteDefinitionProvider::class,
69            CmsRouteDefinitionProvider::class,
70            RouteDefinitionProviderInterface::class,
71            HasRouteDefinition::class,
72            UrlRouteDefinition::class,
73            RequestMethod::class,
74            UrlPrefixList::class,
75            ActionHashmap::class,
76            ApieLib::class,
77            AttributesRoute::class,
78        ];
79        if (!empty($this->scanBoundedContexts['search_path'])) {
80            if (!is_dir($this->scanBoundedContexts['search_path'])) {
81                if (!@mkdir($this->scanBoundedContexts['search_path'], recursive: true)) {
82                    $this->logger->error('I could not create path: "' . $this->scanBoundedContexts['search_path'] . '"');
83                }
84            }
85            if (is_dir($this->scanBoundedContexts['search_path'])) {
86                $routes->addResource(new GlobResource($this->scanBoundedContexts['search_path'], '*', true));
87            } else {
88                $routes->addResource(new FileExistenceResource($this->scanBoundedContexts['search_path']));
89            }
90        }
91        
92        foreach ($classesForCaching as $classForCaching) {
93            if (is_object($classForCaching) || class_exists($classForCaching)) {
94                $routes->addResource(new ReflectionClassResource(new ReflectionClass($classForCaching)));
95            }
96        }
97        $pathsHandled = [];
98        foreach ($this->boundedContextHashmap as $boundedContext) {
99            foreach ($boundedContext->resources as $resource) {
100                $routes->addResource(new ReflectionClassResource($resource));
101                $path = dirname($resource->getFileName());
102                if (!isset($pathsHandled[$path])) {
103                    $pathsHandled[$path] = true;
104                    $routes->addResource(new DirectoryResource($path));
105                }
106            }
107        }
108        $apieContext = $this->contextBuilder->createGeneralContext([
109            'route-gen' => true,
110        ]);
111        if ($this->routeProvider instanceof GlobalRouteDefinitionProviderInterface) {
112            foreach ($this->routeProvider->getGlobalRoutes() as $routeDefinition) {
113                $routes->addResource(new ReflectionClassResource(new ReflectionClass($routeDefinition)));
114                /** @var HasRouteDefinition $routeDefinition */
115
116                $requirements = [];
117                $url = $routeDefinition->getUrl();
118                $placeholders = $url->getPlaceholders();
119                if (in_array('properties', $placeholders)) {
120                    $requirements['properties'] = '[a-zA-Z0-9]+(/[a-zA-Z0-9]+)*';
121                }
122                if (in_array('path', $placeholders)) {
123                    $requirements['path'] = '.*';
124                }
125                $path = ltrim($url, '/');
126                $method = $routeDefinition->getMethod();
127                $defaults = $routeDefinition->getRouteAttributes()
128                    + [
129                        '_controller' => $routeDefinition->getController(),
130                        '_is_apie' => true,
131                    ];
132                $route = (new Route($path, $defaults, $requirements))->setMethods($method->toSymfonyRequestMethod());
133                $routes->add(
134                    'apie._global.' . $routeDefinition->getOperationId(),
135                    $route
136                );
137            }
138        }
139        foreach ($this->boundedContextHashmap as $boundedContextId => $boundedContext) {
140            $subcontext = $apieContext->withContext(BoundedContext::class, $boundedContext)
141                ->withContext(BoundedContextId::class, $boundedContext->getId())
142                ->withContext(ContextConstants::BOUNDED_CONTEXT_ID, $boundedContextId);
143            foreach ($this->routeProvider->getActionsForBoundedContext($boundedContext, $subcontext) as $routeDefinition) {
144                $routes->addResource(new ReflectionClassResource(new ReflectionClass($routeDefinition)));
145                /** @var HasRouteDefinition $routeDefinition */
146                $prefix = $this->routePrefixProvider->getPossiblePrefixes($routeDefinition);
147
148                $requirements = $prefix->getRouteRequirements();
149                $url = $routeDefinition->getUrl();
150                $placeholders = $url->getPlaceholders();
151                if (in_array('properties', $placeholders)) {
152                    $requirements['properties'] = '[a-zA-Z0-9]+(/[a-zA-Z0-9]+)*';
153                }
154                if (in_array('path', $placeholders)) {
155                    $requirements['path'] = '.*';
156                }
157                $path = $prefix . $boundedContextId . '/' . ltrim($url, '/');
158                $method = $routeDefinition->getMethod();
159                $defaults = $routeDefinition->getRouteAttributes()
160                    + [
161                        '_controller' => $routeDefinition->getController(),
162                        '_is_apie' => true,
163                    ];
164                $route = (new Route($path, $defaults, $requirements))->setMethods([$method->value]);
165                $routes->add(
166                    'apie.' . $boundedContextId . '.' . $routeDefinition->getOperationId(),
167                    $route
168                );
169            }
170        }
171        
172        return $routes;
173    }
174
175    /**
176     * @param mixed $resource
177     * @param mixed $type
178     */
179    public function supports($resource, $type = null): bool
180    {
181        return 'apie' === $type;
182    }
183}