Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
50.00% |
4 / 8 |
CRAP | |
68.97% |
60 / 87 |
SmalldbExtension | |
0.00% |
0 / 1 |
|
50.00% |
4 / 8 |
62.61 | |
68.97% |
60 / 87 |
getConfiguration | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
load | |
0.00% |
0 / 1 |
5.14 | |
82.14% |
23 / 28 |
|||
process | |
0.00% |
0 / 1 |
2.86 | |
40.00% |
2 / 5 |
|||
generateClasses | |
100.00% |
1 / 1 |
5 | |
100.00% |
15 / 15 |
|||
registerProvider | |
100.00% |
1 / 1 |
6 | |
100.00% |
8 / 8 |
|||
createClassLocator | |
0.00% |
0 / 1 |
28.91 | |
23.53% |
4 / 17 |
|||
registerSources | |
100.00% |
1 / 1 |
5 | |
100.00% |
7 / 7 |
|||
createCheff | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
<?php | |
/* | |
* Copyright (c) 2017-2019, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\StateMachine\SymfonyDI; | |
use Smalldb\ClassLocator\BrokenClassLogger; | |
use Smalldb\ClassLocator\ClassLocator; | |
use Smalldb\ClassLocator\CompositeClassLocator; | |
use Smalldb\CodeCooker\Chef; | |
use Smalldb\CodeCooker\Cookbook; | |
use Smalldb\CodeCooker\RecipeLocator; | |
use Smalldb\StateMachine\AccessControlExtension\SimpleTransitionGuard; | |
use Smalldb\StateMachine\ClassGenerator\GeneratedClassAutoloader; | |
use Smalldb\StateMachine\ClassGenerator\SmalldbClassGenerator; | |
use Smalldb\StateMachine\Provider\LambdaProvider; | |
use Smalldb\StateMachine\Smalldb; | |
use Smalldb\StateMachine\SmalldbDefinitionBag; | |
use Smalldb\StateMachine\SmalldbDefinitionBagInterface; | |
use Smalldb\StateMachine\SmalldbDefinitionBagReader; | |
use Smalldb\ClassLocator\ComposerClassLocator; | |
use Smalldb\ClassLocator\Psr4ClassLocator; | |
use Smalldb\ClassLocator\RealPathList; | |
use Smalldb\StateMachine\SourcesExtension\Definition\SourceClassFile; | |
use Smalldb\StateMachine\SourcesExtension\Definition\SourcesExtension; | |
use Smalldb\StateMachine\Transition\TransitionGuard; | |
use Symfony\Component\Config\Resource\FileResource; | |
use Symfony\Component\Config\Resource\ReflectionClassResource; | |
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; | |
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | |
use Symfony\Component\DependencyInjection\ContainerBuilder; | |
use Symfony\Component\DependencyInjection\Definition; | |
use Symfony\Component\DependencyInjection\Extension\Extension; | |
use Symfony\Component\DependencyInjection\Reference; | |
class SmalldbExtension extends Extension implements CompilerPassInterface | |
{ | |
protected const PROVIDER_CLASS = LambdaProvider::class; | |
protected array $config; | |
private BrokenClassLogger $brokenClassLogger; | |
/** | |
* Create bundle configuration. Smalldb Bundle overrides this with | |
* an extended configuration class. | |
* | |
* @return Configuration | |
*/ | |
public function getConfiguration(array $config, ContainerBuilder $container) | |
{ | |
return new Configuration(); | |
} | |
public function load(array $configs, ContainerBuilder $container) | |
{ | |
// Get configuration | |
$this->config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); | |
// Define Smalldb entry point | |
$smalldb = $container->autowire(Smalldb::class, Smalldb::class) | |
->setPublic(true); | |
$baseDir = $container->getParameter('kernel.project_dir'); | |
$classLocator = $this->createClassLocator($baseDir); | |
$classLocator->setBrokenClassHandler($this->brokenClassLogger = new BrokenClassLogger()); | |
// Code Cooker: Generate classes | |
if (!empty($this->config['code_cooker']) && ($this->config['code_cooker']['enable'] ?? false)) { | |
$cheff = $this->createCheff($classLocator, $container); | |
// FIXME: Cook on demand only | |
$cheff->cookAllRecipes(); | |
if ($this->config['code_cooker']['enable_autoloader_generator'] ?? false) { | |
$cheff->registerLoadingAutoloader(); | |
} | |
} | |
// Register autoloader for generated classes | |
$genNamespace = $this->config['class_generator']['namespace'] ?? 'Smalldb\\GeneratedCode\\'; | |
$genPath = $this->config['class_generator']['path'] ?? $container->getParameter('kernel.cache_dir') . '/smalldb'; | |
$smalldb->addMethodCall('registerGeneratedClassAutoloader', [$genNamespace, $genPath]); | |
$autoloader = new GeneratedClassAutoloader($genNamespace, $genPath); | |
$autoloader->registerLoader(); | |
// Load all state machine definitions | |
$definitionReader = new SmalldbDefinitionBagReader(); | |
if (empty($this->config['definition_classes'])) { | |
$definitionReader->addFromClassLocator($classLocator); | |
} else { | |
$definitionReader->addFromAnnotatedClasses($this->config['definition_classes']); | |
} | |
$definitionBag = $definitionReader->getDefinitionBag(); | |
// Generate everything | |
$this->generateClasses($container, $smalldb, $definitionBag, $genNamespace, $genPath); | |
// Register source files for automatic container reload | |
$this->registerSources($definitionBag, $container); | |
// Register Guard | |
$container->autowire(TransitionGuard::class, SimpleTransitionGuard::class) | |
->setArguments([ | |
SimpleTransitionGuard::compileTransitionPredicatesSymfony($definitionBag, $container), | |
!empty($this->config['access_control']['default_allow']) | |
]); | |
} | |
public function process(ContainerBuilder $container) | |
{ | |
foreach ($this->brokenClassLogger->getBrokenClasses() | |
as ['className' => $className, 'fileAbsPath' => $fileAbsPath, 'reason' => $reason]) | |
{ | |
$container->log($this, sprintf("Ignoring a broken class \"%s\": %s", $className, $reason->getMessage())); | |
$container->addResource(new FileResource($fileAbsPath)); // Rebuild the container if the broken file gets fixed. | |
} | |
// ... | |
} | |
private function generateClasses(ContainerBuilder $container, Definition $smalldb, | |
SmalldbDefinitionBag $definitionBag, | |
string $generatedCodeNamespace, string $generatedCodePath) | |
{ | |
// Code generator | |
$generator = new SmalldbClassGenerator($generatedCodeNamespace, $generatedCodePath); | |
// Generate reference implementations and register providers | |
foreach ($definitionBag->getAllDefinitions() as $machineType => $definition) { | |
$referenceClass = $definition->getReferenceClass(); | |
$repositoryClass = $definition->getRepositoryClass(); | |
$transitionsClass = $definition->getTransitionsClass(); | |
$realReferenceClass = $referenceClass ? $generator->generateReferenceClass($referenceClass, $definition) : null; | |
$providerId = "smalldb.$machineType.provider"; | |
// Register the provider | |
$this->registerProvider($container, $providerId, $machineType, $realReferenceClass, | |
new Reference(SmalldbDefinitionBagInterface::class), | |
$transitionsClass ? new Reference($transitionsClass) : null, | |
$repositoryClass ? new Reference($repositoryClass) : null); | |
// Register state machine type | |
$smalldb->addMethodCall('registerMachineType', [new Reference($providerId), [$referenceClass]]); | |
} | |
// Generate static definition bag | |
$generatedBag = $generator->generateDefinitionBag($definitionBag); | |
$container->register(SmalldbDefinitionBagInterface::class, $generatedBag); | |
} | |
protected function registerProvider(ContainerBuilder $container, string $providerId, $machineType, | |
?string $realReferenceClass, Reference $definitionBagReference, | |
?Reference $transitionsReference, ?Reference $repositoryReference): Definition | |
{ | |
// Glue them together using a machine provider | |
return $container->autowire($providerId, static::PROVIDER_CLASS) | |
->setFactory(LambdaProvider::class . '::createWithDefinitionBag') | |
->setArguments([ | |
$machineType, | |
$realReferenceClass, | |
$definitionBagReference, | |
$transitionsReference ? new ServiceClosureArgument($transitionsReference) : null, | |
$repositoryReference ? new ServiceClosureArgument($repositoryReference) : null, | |
]); | |
} | |
protected function createClassLocator(string $baseDir): ClassLocator | |
{ | |
$classLocator = new CompositeClassLocator(); | |
if (!empty($this->config['class_locator'])) { | |
$classLocatorConfig = $this->config['class_locator']; | |
if (!empty($classLocatorConfig['include_dirs'])) { | |
$includeList = new RealPathList($baseDir, $classLocatorConfig['include_dirs']); | |
} else { | |
$includeList = null; | |
} | |
if (!empty($classLocatorConfig['exclude_dirs'])) { | |
$excludeList = new RealPathList($baseDir, $classLocatorConfig['exclude_dirs']); | |
} else { | |
$excludeList = null; | |
} | |
if (!empty($classLocatorConfig['psr4_dirs'])) { | |
foreach ($classLocatorConfig['psr4_dirs'] as $namespace => $dir) { | |
$classLocator->addClassLocator(new Psr4ClassLocator($namespace, $dir)); | |
} | |
} | |
if (!empty($classLocatorConfig['use_composer'])) { | |
$excludeVendorDir = !empty($classLocatorConfig['ignore_vendor_dir']); | |
$classLocator->addClassLocator(new ComposerClassLocator($baseDir, $includeList, $excludeList, $excludeVendorDir)); | |
} | |
} else { | |
$classLocator->addClassLocator(new ComposerClassLocator($baseDir, [], [], true)); | |
} | |
return $classLocator; | |
} | |
private function registerSources(SmalldbDefinitionBag $definitionBag, ContainerBuilder $container) | |
{ | |
foreach ($definitionBag->getAllDefinitions() as $definition) { | |
if (($ext = $definition->findExtension(SourcesExtension::class))) { | |
foreach ($ext->getSourceFiles() as $source) { | |
if ($source instanceof SourceClassFile) { | |
$container->addResource(new ReflectionClassResource(new \ReflectionClass($source->getClassname()))); | |
} else { | |
$container->addResource(new FileResource($source->getFilename())); | |
} | |
} | |
} | |
} | |
} | |
private function createCheff(ClassLocator $classLocator, ContainerBuilder $container): Chef | |
{ | |
$cookbook = new Cookbook(); | |
$recipeLocator = new RecipeLocator($classLocator); | |
$recipeLocator->onRecipeClass(function (\ReflectionClass $sourceClass) use ($container) { | |
$container->addResource(new ReflectionClassResource($sourceClass)); | |
}); | |
$cookbook->addRecipes($recipeLocator->locateRecipes()); | |
return new Chef($cookbook, $classLocator); | |
} | |
} | |