Source of file TraitConfigTransformer.php
Size: 11,297 Bytes - Last Modified: 2021-12-24T06:50:40+00:00
/var/www/docs.ssmods.com/process/src/src/TraitConfigTransformer.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 | <?php namespace hchokshi\SilverStripe\TraitConfig; use ReflectionClass; use ReflectionProperty; use SilverStripe\Config\Collections\CachedConfigCollection; use SilverStripe\Config\Collections\ConfigCollectionInterface; use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Collections\MutableConfigCollectionInterface; use SilverStripe\Config\MergeStrategy\Priority; use SilverStripe\Config\Transformer\PrivateStaticTransformer; use SilverStripe\Config\Transformer\TransformerInterface; use SilverStripe\Core\Kernel; use SilverStripe\Core\Manifest\ClassLoader; /** * SilverStripe config transformer that takes configuration from traits and merges it into using classes, using aliases * to avoid private static conflicts. * @package hchokshi\SilverStripe\TraitConfig */ class TraitConfigTransformer implements TransformerInterface { const ALIAS_PHPDOC = '@aliasConfig'; /** * @var array */ protected $traitConfigCache = []; /** * @var array */ protected $ownStaticCache = []; /** * @var array */ protected $configMapCache = []; /** * @var array|callable */ protected $classes; /** * @param array|callable $classes List of classes, or callback to lazy-load. */ public function __construct($classes) { $this->classes = $classes; } /** * Add TraitConfigTransformer to a kernel's config manifest. * @param Kernel $kernel */ public static function addToKernel(Kernel $kernel) { /** @var CachedConfigCollection $config */ $config = $kernel->getConfigLoader()->getManifest(); $existingCollectionCreator = $config->getCollectionCreator(); $config->setCollectionCreator(function () use ($existingCollectionCreator) { /** @var MemoryConfigCollection $existingCollection */ $existingCollection = $existingCollectionCreator(); return $existingCollection->transform([ new static(function () { return ClassLoader::inst()->getManifest()->getClassNames(); }), ]); }); } /** * @inheritDoc */ public function transform(MutableConfigCollectionInterface $collection) { foreach ($this->getClasses() as $class) { try { $reflectionClass = new ReflectionClass($class); foreach ($reflectionClass->getTraits() as $trait) { $this->mergeTraitConfig($collection, $class, $trait); } } catch (\ReflectionException $e) { // Class doesn't exist continue; } } return $collection; } /** * Get list of defined classes in the manifest. * @see PrivateStaticTransformer::getClasses() * @return array */ protected function getClasses() { if (is_callable($this->classes)) { $this->classes = call_user_func($this->classes); } return $this->classes; } /** * Merge a used trait's config into $class's config. * @param MutableConfigCollectionInterface $collection * @param $class * @param ReflectionClass $trait */ protected function mergeTraitConfig(MutableConfigCollectionInterface $collection, $class, ReflectionClass $trait) { $traitConfig = $this->getTraitConfig($collection, $trait); if (empty($traitConfig)) return; // Merge in trait config, giving the class a higher priority $classConfig = Priority::mergeArray( $collection->get($class, null, true), $traitConfig ); $collection->set($class, null, $classConfig, [ 'from_trait' => $trait->getName(), ]); } /** * Get the config for a trait to be merged into using class, with aliasing applied. * @param ConfigCollectionInterface $collection * @param ReflectionClass $trait * @return array */ protected function getTraitConfig(ConfigCollectionInterface $collection, ReflectionClass $trait) { return $this->getCachedOrCall($this->traitConfigCache, $trait->getName(), function () use ($trait, $collection) { $configFromNestedTraits = []; foreach ($trait->getTraits() as $nestedTrait) { $configFromNestedTraits = Priority::mergeArray( $this->getTraitConfig($collection, $nestedTrait), $configFromNestedTraits ); } $ownYaml = $collection->get($trait->getName(), null, true); $ownMappedYaml = $this->applyTraitConfigAliases($trait, $ownYaml); $ownMappedStatics = $this->applyTraitConfigAliases($trait, $this->getTraitOwnAliasedStatics($collection, $trait)); // Merge trait YAML over trait statics $traitConfig = Priority::mergeArray( $ownMappedYaml, $ownMappedStatics ); // Merge trait's owm config over nested trait config return Priority::mergeArray($traitConfig, $configFromNestedTraits); }); } /** * Get a cached value, transparently calling a function and caching the result if not already cached. * @param array $cache * @param string $cacheKey * @param callable $source * @return mixed */ protected function getCachedOrCall(array &$cache, $cacheKey, callable $source) { if (!isset($cache[$cacheKey])) { $cache[$cacheKey] = $source(); } return $cache[$cacheKey]; } /** * Apply aliases to a trait's config. * @param ReflectionClass $trait * @param array $config * @return array */ protected function applyTraitConfigAliases(ReflectionClass $trait, array $config) { $mappedValues = []; $unmappedValues = []; $map = $this->getTraitConfigMap($trait); foreach ($config as $source => $value) { if (!isset($map[$source])) { // Unmapped is simple - it can only be defined once per config. Set and move on. $unmappedValues[$source] = $value; continue; } $dest = $map[$source]; if (isset($map[$dest])) { // Another value was already mapped to $dest - merge current over it. $mappedValues = Priority::mergeArray([ $dest => $value, ], $mappedValues); } else { $mappedValues[$dest] = $value; } } // When a value is defined as both its proper name and by an aliased name, prioritise the explicit name over the alias. return Priority::mergeArray($unmappedValues, $mappedValues); } /** * Get a map of (trait config name) => (actual config name). * @param ReflectionClass $trait * @return array */ protected function getTraitConfigMap(ReflectionClass $trait) { return $this->getCachedOrCall($this->configMapCache, $trait->getName(), function () use ($trait) { $configMap = []; foreach ($this->getAliasedConfigProperties($trait) as $property) { $mergeInto = $this->getAliasedConfigName($property); if ($mergeInto !== null) { $configMap[$property->getName()] = $mergeInto; } } return $configMap; }); } /** * Get the config private static properties for a trait that are aliased. * @param ReflectionClass $trait * @return ReflectionProperty[] */ protected function getAliasedConfigProperties(ReflectionClass $trait) { $properties = []; foreach ($trait->getProperties(ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_STATIC) as $property) { if ($this->isConfigAliasProperty($property)) { $properties[] = $property; } } return $properties; } /** * Check if a property is a config alias. * @param ReflectionProperty $prop * @return bool */ protected function isConfigAliasProperty(ReflectionProperty $prop) { $docComment = $prop->getDocComment(); return $prop->isPrivate() && strpos($docComment, '@internal') !== false && strpos($docComment, static::ALIAS_PHPDOC) !== false; } /** * Determine the config name a trait config property maps to. * @param ReflectionProperty $property * @return string|null Config name to map property value to, or null to ignore property. */ protected function getAliasedConfigName(ReflectionProperty $property) { if (preg_match('/' . static::ALIAS_PHPDOC . '(\s+)\$([^\s]+)/', $property->getDocComment(), $matches)) { foreach (token_get_all("<?php {$matches[0]}") as $token) { if ($token[0] === T_VARIABLE) { // Strip $ from front of variable return substr($token[1], 1); } } } return null; } /** * Get the config properties defined by a trait, ignoring properties defined by nested traits. * @param ConfigCollectionInterface $collection * @param ReflectionClass $trait * @return array */ protected function getTraitOwnAliasedStatics(ConfigCollectionInterface $collection, ReflectionClass $trait) { return $this->getCachedOrCall($this->ownStaticCache, $trait->getName(), function () use ($trait, $collection) { $ownStatics = []; $nestedTraitStatics = []; foreach ($trait->getTraits() as $nestedTrait) { $nestedTraitStatics = Priority::mergeArray( $this->getTraitOwnAliasedStatics($collection, $nestedTrait), $nestedTraitStatics ); } foreach ($this->getAliasedConfigProperties($trait) as $property) { $propName = $property->getName(); $property->setAccessible(true); if (isset($nestedTraitStatics[$propName]) || !$this->isConfigValue($property->getValue())) { // Skip non-config values and nested trait statics continue; } $ownStatics[$propName] = $property->getValue(); } return $ownStatics; }); } /** * Detect if a value is a valid config value. * @see PrivateStaticTransformer::isConfigValue() * @param mixed $input * @return true */ protected function isConfigValue($input) { if (is_array($input)) { foreach ($input as $next) { if (!$this->isConfigValue($next)) { return false; } } } return !is_object($input) && !is_resource($input); } } |