diff --git a/.circleci/config.yml b/.circleci/config.yml index 90dc34bb..c7f48c4a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ jobs: # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor docker: # Specify the version you desire here - - image: cimg/php:8.0.8 + - image: cimg/php:8.3.27 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images diff --git a/Attribute/InjectImplementations.php b/Attribute/InjectImplementations.php index f9088a1e..69049b8f 100644 --- a/Attribute/InjectImplementations.php +++ b/Attribute/InjectImplementations.php @@ -27,7 +27,11 @@ #[Attribute(Attribute::TARGET_METHOD)] final class InjectImplementations { - public function __construct(private string $interface) {} + public function __construct(private string $interface) { + if (!interface_exists($interface)) { + throw new \InvalidArgumentException("The interface '{$interface}' used in an InjectImplementations attribute does not exist."); + } + } /** * Gets the name of the interface the marked method collects. diff --git a/Definition/DeferredGeneratorDefinition.php b/Definition/DeferredGeneratorDefinition.php index ab3b281c..6fdd3fc9 100644 --- a/Definition/DeferredGeneratorDefinition.php +++ b/Definition/DeferredGeneratorDefinition.php @@ -26,7 +26,7 @@ class DeferredGeneratorDefinition extends PropertyDefinition implements Generato public function __construct( string $data_type, protected string $componentType, - protected string $generatorClass, + public readonly string $generatorClass, ) { parent::__construct($data_type); } diff --git a/Definition/MergingGeneratorDefinition.php b/Definition/MergingGeneratorDefinition.php index 09d4c448..d4c71b4d 100644 --- a/Definition/MergingGeneratorDefinition.php +++ b/Definition/MergingGeneratorDefinition.php @@ -37,7 +37,7 @@ class MergingGeneratorDefinition extends PropertyDefinition implements Generator public function __construct( string $data_type, protected string $componentType, - protected string $generatorClass, + public readonly string $generatorClass, ) { parent::__construct($data_type); } diff --git a/Definition/OptionDefinition.php b/Definition/OptionDefinition.php new file mode 100644 index 00000000..c6c45f74 --- /dev/null +++ b/Definition/OptionDefinition.php @@ -0,0 +1,44 @@ +componentType; } + /** + * {@inheritdoc} + */ public function getDeltaDefinition(): self { $delta_definition = parent::getDeltaDefinition(); @@ -45,7 +60,19 @@ public function getDeltaDefinition(): self { return $delta_definition; } - public function addOption(OptionDefinition $option): self { + /** + * Adds an option to this definition's list of options. + * + * This may not be called if this definition uses an options provider. + * + * @param \MutableTypedData\Definition\OptionDefinition $option + * + * @return self + * + * @throws \MutableTypedData\Exception\InvalidDefinitionException + * Throws an exception if this definition uses an options provider. + */ + public function addOption(BaseOptionDefinition $option): self { if ($this->optionsProvider) { throw new InvalidDefinitionException("Can't add options if using an options provider."); } @@ -53,10 +80,17 @@ public function addOption(OptionDefinition $option): self { return parent::addOption($option); } + /** + * {@inheritdoc} + */ public function hasOptions(): bool { + // Handle options providers. return parent::hasOptions() || !empty($this->optionsProvider); } + /** + * {@inheritdoc} + */ public function getOptions(): array { if (!$this->options && $this->optionsProvider) { $this->options = $this->optionsProvider->getOptions(); @@ -73,15 +107,29 @@ public function removeDefault(): self { return $this; } + /** + * Sets the variant mapping provider. + * + * @param VariantMappingProviderInterface $provider + * The provider object. + * + * @return self + */ public function setVariantMappingProvider(VariantMappingProviderInterface $provider): self { $this->variantMappingProvider = $provider; return $this; } + /** + * {@inheritdoc} + */ public function hasVariantMapping(): bool { return parent::hasVariantMapping() || $this->variantMappingProvider; } + /** + * {@inheritdoc} + */ public function getVariantMapping(): ?array { if (!$this->variantMapping && $this->variantMappingProvider) { $this->variantMapping = $this->variantMappingProvider->getVariantMapping(); @@ -212,7 +260,13 @@ public function setPresets(...$presets) :self { $options = []; foreach ($presets as $key => $preset) { - $option = OptionDefinition::create($key, $preset['label'], $preset['description'] ?? NULL); + $option = OptionDefinition::create( + $key, + $preset['label'], + $preset['description'] ?? NULL, + // TODO: These are only supported for old-style array definitions! + api_url: $preset['api_url'] ?? NULL, + ); $options[] = $option; } } @@ -229,6 +283,21 @@ public function getPresets() :array { return $this->presets; } + /** + * Sets a processing callback. + * + * Processing is applied to a component's data when it is instantiated from + * input data. + * + * Note that processing is not applied to default values! + * + * @param callable $callback + * The callback to apply to data. + * + * @return self + * + * @see \DrupalCodeBuilder\Task\Generate\ComponentCollector::processComponentData() + */ public function setProcessing(callable $callback): self { $this->processing = $callback; @@ -256,6 +325,27 @@ public function loadLazyProperties() { } } + /** + * Sets dependent values. + * + * UIs can use these to determine whether to show a property. + * + * @param array $dependent_value + * An array of dependencies which this property may require to be shown. + * Keys are relative addresses. Values are either the target value, or for + * string data, TRUE to represent that the target property must be filled. + * + * @return self + */ + public function setDependencyValue(array $dependent_value): self { + $this->dependentValue = $dependent_value; + return $this; + } + + public function getDependencyValue(): ?array { + return $this->dependentValue; + } + public function offsetExists(mixed $offset): bool { dump($this); throw new \Exception("Accessing definition $this->name as array with offsetExists $offset."); diff --git a/Definition/PropertyInsertTrait.php b/Definition/PropertyManipulationTrait.php similarity index 58% rename from Definition/PropertyInsertTrait.php rename to Definition/PropertyManipulationTrait.php index 5b245797..9a6c59d9 100644 --- a/Definition/PropertyInsertTrait.php +++ b/Definition/PropertyManipulationTrait.php @@ -8,7 +8,7 @@ /** * Provides methods to insert properties. */ -trait PropertyInsertTrait { +trait PropertyManipulationTrait { /** * Adds properties before the named property. @@ -50,6 +50,66 @@ public function addPropertyAfter(string $after, PropertyDefinition ...$propertie return $this; } + /** + * Moves a property to be before another. + * + * @param string $move_property_name + * The name of the property to move. + * @param string $before_property_name + * The name of the property before which the moved property should go. + * + * @throws \InvalidArgumentException + * Throws an exception if either property does not exist. + * + * @return \MutableTypedData\Definition\DataDefinition + * Returns the same instance for chaining. + */ + public function movePropertyBefore(string $move_property_name, string $before_property_name): self { + if (!isset($this->properties[$move_property_name])) { + throw new \InvalidArgumentException("No property '$move_property_name'."); + } + if (!isset($this->properties[$before_property_name])) { + throw new \InvalidArgumentException("No property '$before_property_name'."); + } + // TODO: Check not being put before variant property. + + $moved_property = $this->properties[$move_property_name]; + unset($this->properties[$move_property_name]); + + InsertArray::insertBefore($this->properties, $before_property_name, [$move_property_name => $moved_property]); + + return $this; + } + + /** + * Moves a property to be after another. + * + * @param string $move_property_name + * The name of the property to move. + * @param string $after_property_name + * The name of the property after which the moved property should go. + * + * @throws \InvalidArgumentException + * Throws an exception if either property does not exist. + * + * @return \MutableTypedData\Definition\DataDefinition + * Returns the same instance for chaining. + */ + public function movePropertyAfter(string $move_property_name, string $after_property_name): void { + if (!isset($this->properties[$move_property_name])) { + throw new \InvalidArgumentException("No property '$move_property_name'."); + } + if (!isset($this->properties[$after_property_name])) { + throw new \InvalidArgumentException("No property '$after_property_name'."); + } + // TODO: Check variant property is not being moved. + + $moved_property = $this->properties[$move_property_name]; + unset($this->properties[$move_property_name]); + + InsertArray::insertAfter($this->properties, $after_property_name, [$move_property_name => $moved_property]); + } + /** * Helper for inserting properties. * diff --git a/Definition/VariantGeneratorDefinition.php b/Definition/VariantGeneratorDefinition.php index a6942473..d5d5d152 100644 --- a/Definition/VariantGeneratorDefinition.php +++ b/Definition/VariantGeneratorDefinition.php @@ -19,7 +19,7 @@ */ class VariantGeneratorDefinition extends VariantDefinition implements PropertyListInterface { - use PropertyInsertTrait; + use PropertyManipulationTrait; /** * Whether properties have been obtained from the generator class yet. diff --git a/Definition/VariantMappingProviderInterface.php b/Definition/VariantMappingProviderInterface.php index 87f50d31..d9d5953c 100644 --- a/Definition/VariantMappingProviderInterface.php +++ b/Definition/VariantMappingProviderInterface.php @@ -7,6 +7,13 @@ */ interface VariantMappingProviderInterface { + /** + * Gets the variant mapping. + * + * @return array + * An array in the same format as + * \MutableTypedData\Definition\DataDefinition::setVariantMapping(). + */ public function getVariantMapping(): array; } diff --git a/DependencyInjection/cache/DrupalCodeBuilderCompiledContainer.php b/DependencyInjection/cache/DrupalCodeBuilderCompiledContainer.php index ce078509..8063a636 100644 --- a/DependencyInjection/cache/DrupalCodeBuilderCompiledContainer.php +++ b/DependencyInjection/cache/DrupalCodeBuilderCompiledContainer.php @@ -45,165 +45,176 @@ class DrupalCodeBuilderCompiledContainer extends DI\CompiledContainer{ 'subEntry17' => 'get39', 'Collect\\HooksCollector9' => 'get40', 'subEntry18' => 'get41', - 'Collect\\MethodCollector' => 'get42', - 'Collect\\PluginTypesCollector' => 'get43', - 'subEntry19' => 'get44', - 'subEntry20' => 'get45', - 'subEntry21' => 'get46', - 'subEntry22' => 'get47', - 'Collect\\ServiceTagTypesCollector' => 'get48', - 'subEntry23' => 'get49', - 'subEntry24' => 'get50', - 'subEntry25' => 'get51', - 'Collect\\ServicesCollector' => 'get52', - 'subEntry26' => 'get53', - 'subEntry27' => 'get54', - 'subEntry28' => 'get55', - 'Collect5' => 'get56', - 'subEntry29' => 'get57', - 'subEntry30' => 'get58', - 'Collect6' => 'get59', - 'subEntry31' => 'get60', - 'subEntry32' => 'get61', - 'Collect7' => 'get62', - 'subEntry33' => 'get63', - 'subEntry34' => 'get64', - 'Configuration' => 'get65', - 'subEntry35' => 'get66', - 'subEntry36' => 'get67', - 'Generate\\ComponentClassHandler' => 'get68', - 'subEntry37' => 'get69', - 'Generate\\ComponentCollector' => 'get70', + 'Collect\\MetadataCollector' => 'get42', + 'Collect\\MethodCollector' => 'get43', + 'Collect\\PluginTypesCollector' => 'get44', + 'subEntry19' => 'get45', + 'subEntry20' => 'get46', + 'subEntry21' => 'get47', + 'subEntry22' => 'get48', + 'Collect\\ServiceTagTypesCollector' => 'get49', + 'subEntry23' => 'get50', + 'subEntry24' => 'get51', + 'subEntry25' => 'get52', + 'Collect\\ServicesCollector' => 'get53', + 'subEntry26' => 'get54', + 'subEntry27' => 'get55', + 'subEntry28' => 'get56', + 'Collect5' => 'get57', + 'subEntry29' => 'get58', + 'subEntry30' => 'get59', + 'Collect6' => 'get60', + 'subEntry31' => 'get61', + 'subEntry32' => 'get62', + 'Collect7' => 'get63', + 'subEntry33' => 'get64', + 'subEntry34' => 'get65', + 'Configuration' => 'get66', + 'subEntry35' => 'get67', + 'subEntry36' => 'get68', + 'Generate\\ComponentClassHandler' => 'get69', + 'subEntry37' => 'get70', 'subEntry38' => 'get71', - 'subEntry39' => 'get72', - 'Generate\\FileAssembler' => 'get73', - 'ReportAdminRoutes' => 'get74', - 'subEntry40' => 'get75', - 'ReportDataTypes' => 'get76', + 'Generate\\ComponentCollector' => 'get72', + 'subEntry39' => 'get73', + 'subEntry40' => 'get74', + 'Generate\\FileAssembler' => 'get75', + 'ReportAdminRoutes' => 'get76', 'subEntry41' => 'get77', - 'ReportElementTypes' => 'get78', + 'ReportDataTypes' => 'get78', 'subEntry42' => 'get79', - 'ReportEntityTypes' => 'get80', + 'ReportElementTypes' => 'get80', 'subEntry43' => 'get81', - 'ReportEventNames' => 'get82', + 'ReportEntityTypes' => 'get82', 'subEntry44' => 'get83', - 'ReportFieldTypes' => 'get84', + 'ReportEventNames' => 'get84', 'subEntry45' => 'get85', - 'ReportHookData' => 'get86', + 'ReportFieldTypes' => 'get86', 'subEntry46' => 'get87', - 'ReportHookDataFolder' => 'get88', + 'ReportHookClassMethodData' => 'get88', 'subEntry47' => 'get89', - 'ReportHookGroups' => 'get90', + 'ReportHookData' => 'get90', 'subEntry48' => 'get91', - 'ReportHookPresets' => 'get92', + 'ReportHookDataFolder' => 'get92', 'subEntry49' => 'get93', - 'ReportPluginData' => 'get94', + 'ReportHookGroups' => 'get94', 'subEntry50' => 'get95', - 'ReportServiceData' => 'get96', + 'ReportHookPresets' => 'get96', 'subEntry51' => 'get97', - 'ReportServiceTags' => 'get98', + 'ReportPluginData' => 'get98', 'subEntry52' => 'get99', - 'ReportSummary' => 'get100', + 'ReportServiceData' => 'get100', 'subEntry53' => 'get101', - 'subEntry54' => 'get102', - 'subEntry55' => 'get103', - 'subEntry56' => 'get104', - 'subEntry57' => 'get105', - 'subEntry58' => 'get106', - 'subEntry59' => 'get107', - 'subEntry60' => 'get108', - 'subEntry61' => 'get109', - 'subEntry62' => 'get110', - 'subEntry63' => 'get111', - 'subEntry64' => 'get112', - 'subEntry65' => 'get113', - 'Testing\\CollectTesting10' => 'get114', - 'subEntry66' => 'get115', - 'subEntry67' => 'get116', - 'subEntry68' => 'get117', - 'subEntry69' => 'get118', - 'subEntry70' => 'get119', - 'subEntry71' => 'get120', - 'subEntry72' => 'get121', - 'subEntry73' => 'get122', - 'subEntry74' => 'get123', - 'subEntry75' => 'get124', - 'subEntry76' => 'get125', - 'subEntry77' => 'get126', - 'subEntry78' => 'get127', - 'Testing\\CollectTesting11' => 'get128', - 'subEntry79' => 'get129', - 'subEntry80' => 'get130', - 'subEntry81' => 'get131', - 'subEntry82' => 'get132', - 'subEntry83' => 'get133', - 'subEntry84' => 'get134', - 'subEntry85' => 'get135', - 'subEntry86' => 'get136', - 'subEntry87' => 'get137', - 'subEntry88' => 'get138', - 'subEntry89' => 'get139', - 'subEntry90' => 'get140', - 'subEntry91' => 'get141', - 'Testing\\CollectTesting7' => 'get142', - 'subEntry92' => 'get143', - 'subEntry93' => 'get144', - 'Testing\\CollectTesting8' => 'get145', + 'ReportServiceTags' => 'get102', + 'subEntry54' => 'get103', + 'ReportSummary' => 'get104', + 'subEntry55' => 'get105', + 'subEntry56' => 'get106', + 'subEntry57' => 'get107', + 'subEntry58' => 'get108', + 'subEntry59' => 'get109', + 'subEntry60' => 'get110', + 'subEntry61' => 'get111', + 'subEntry62' => 'get112', + 'subEntry63' => 'get113', + 'subEntry64' => 'get114', + 'subEntry65' => 'get115', + 'subEntry66' => 'get116', + 'subEntry67' => 'get117', + 'subEntry68' => 'get118', + 'Testing\\CollectTesting10' => 'get119', + 'subEntry69' => 'get120', + 'subEntry70' => 'get121', + 'subEntry71' => 'get122', + 'subEntry72' => 'get123', + 'subEntry73' => 'get124', + 'subEntry74' => 'get125', + 'subEntry75' => 'get126', + 'subEntry76' => 'get127', + 'subEntry77' => 'get128', + 'subEntry78' => 'get129', + 'subEntry79' => 'get130', + 'subEntry80' => 'get131', + 'subEntry81' => 'get132', + 'subEntry82' => 'get133', + 'Testing\\CollectTesting11' => 'get134', + 'subEntry83' => 'get135', + 'subEntry84' => 'get136', + 'subEntry85' => 'get137', + 'subEntry86' => 'get138', + 'subEntry87' => 'get139', + 'subEntry88' => 'get140', + 'subEntry89' => 'get141', + 'subEntry90' => 'get142', + 'subEntry91' => 'get143', + 'subEntry92' => 'get144', + 'subEntry93' => 'get145', 'subEntry94' => 'get146', 'subEntry95' => 'get147', 'subEntry96' => 'get148', - 'subEntry97' => 'get149', - 'subEntry98' => 'get150', - 'subEntry99' => 'get151', - 'subEntry100' => 'get152', - 'subEntry101' => 'get153', - 'subEntry102' => 'get154', - 'subEntry103' => 'get155', - 'subEntry104' => 'get156', - 'subEntry105' => 'get157', - 'subEntry106' => 'get158', - 'Testing\\CollectTesting9' => 'get159', - 'subEntry107' => 'get160', - 'subEntry108' => 'get161', - 'subEntry109' => 'get162', - 'subEntry110' => 'get163', - 'subEntry111' => 'get164', - 'subEntry112' => 'get165', - 'subEntry113' => 'get166', - 'subEntry114' => 'get167', - 'subEntry115' => 'get168', - 'subEntry116' => 'get169', - 'subEntry117' => 'get170', - 'subEntry118' => 'get171', - 'subEntry119' => 'get172', - 'Generate|module' => 'get173', - 'Generate|profile' => 'get174', - 'Collect\\HooksCollector' => 'get175', - 'Collect' => 'get176', - 'Collect.unversioned' => 'get177', - 'subEntry120' => 'get178', - 'subEntry121' => 'get179', - 'subEntry122' => 'get180', - 'subEntry123' => 'get181', - 'subEntry124' => 'get182', - 'subEntry125' => 'get183', - 'subEntry126' => 'get184', - 'subEntry127' => 'get185', - 'subEntry128' => 'get186', - 'subEntry129' => 'get187', - 'subEntry130' => 'get188', - 'subEntry131' => 'get189', - 'subEntry132' => 'get190', - 'Testing\\CollectTesting' => 'get191', - 'DrupalCodeBuilder\\Task\\Collect\\HooksCollector' => 'get192', - 'DrupalCodeBuilder\\Task\\Generate\\ComponentClassHandler' => 'get193', - 'subEntry133' => 'get194', - 'DrupalCodeBuilder\\Task\\Collect\\ContainerBuilderGetter' => 'get195', - 'DrupalCodeBuilder\\Task\\Collect\\MethodCollector' => 'get196', - 'DrupalCodeBuilder\\Task\\Collect\\CodeAnalyser' => 'get197', - 'subEntry134' => 'get198', - 'DrupalCodeBuilder\\Task\\ReportHookData' => 'get199', - 'subEntry135' => 'get200', + 'Testing\\CollectTesting7' => 'get149', + 'subEntry97' => 'get150', + 'subEntry98' => 'get151', + 'Testing\\CollectTesting8' => 'get152', + 'subEntry99' => 'get153', + 'subEntry100' => 'get154', + 'subEntry101' => 'get155', + 'subEntry102' => 'get156', + 'subEntry103' => 'get157', + 'subEntry104' => 'get158', + 'subEntry105' => 'get159', + 'subEntry106' => 'get160', + 'subEntry107' => 'get161', + 'subEntry108' => 'get162', + 'subEntry109' => 'get163', + 'subEntry110' => 'get164', + 'subEntry111' => 'get165', + 'subEntry112' => 'get166', + 'Testing\\CollectTesting9' => 'get167', + 'subEntry113' => 'get168', + 'subEntry114' => 'get169', + 'subEntry115' => 'get170', + 'subEntry116' => 'get171', + 'subEntry117' => 'get172', + 'subEntry118' => 'get173', + 'subEntry119' => 'get174', + 'subEntry120' => 'get175', + 'subEntry121' => 'get176', + 'subEntry122' => 'get177', + 'subEntry123' => 'get178', + 'subEntry124' => 'get179', + 'subEntry125' => 'get180', + 'subEntry126' => 'get181', + 'Generate|module' => 'get182', + 'Generate|profile' => 'get183', + 'Collect\\HooksCollector' => 'get184', + 'Collect' => 'get185', + 'Collect.unversioned' => 'get186', + 'subEntry127' => 'get187', + 'subEntry128' => 'get188', + 'subEntry129' => 'get189', + 'subEntry130' => 'get190', + 'subEntry131' => 'get191', + 'subEntry132' => 'get192', + 'subEntry133' => 'get193', + 'subEntry134' => 'get194', + 'subEntry135' => 'get195', + 'subEntry136' => 'get196', + 'subEntry137' => 'get197', + 'subEntry138' => 'get198', + 'subEntry139' => 'get199', + 'subEntry140' => 'get200', + 'Testing\\CollectTesting' => 'get201', + 'DrupalCodeBuilder\\Task\\Collect\\HooksCollector' => 'get202', + 'DrupalCodeBuilder\\Task\\Generate\\ComponentClassHandler' => 'get203', + 'subEntry141' => 'get204', + 'subEntry142' => 'get205', + 'DrupalCodeBuilder\\Task\\Collect\\ContainerBuilderGetter' => 'get206', + 'DrupalCodeBuilder\\Task\\Collect\\MethodCollector' => 'get207', + 'DrupalCodeBuilder\\Task\\Collect\\CodeAnalyser' => 'get208', + 'subEntry143' => 'get209', + 'DrupalCodeBuilder\\Task\\ReportHookData' => 'get210', + 'subEntry144' => 'get211', ); protected function get1() @@ -222,6 +233,10 @@ protected function get3() return [ 'AdminSettingsForm' => [ 7 => 'AdminSettingsForm7', + ], + 'ContentEntityType' => [ + 9 => 'ContentEntityType9AndLower', + 8 => 'ContentEntityType9AndLower', ], 'DrushCommand' => [ 11 => 'DrushCommand', @@ -477,184 +492,184 @@ protected function get40() } protected function get42() + { + $object = new \DrupalCodeBuilder\Task\Collect\MetadataCollector(); + return $object; + } + + protected function get43() { $object = new \DrupalCodeBuilder\Task\Collect\MethodCollector(); return $object; } - protected function get44() + protected function get45() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get45() + protected function get46() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\ContainerBuilderGetter'); } - protected function get46() + protected function get47() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\MethodCollector'); } - protected function get47() + protected function get48() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\CodeAnalyser'); } - protected function get43() + protected function get44() { - $object = new \DrupalCodeBuilder\Task\Collect\PluginTypesCollector($this->get44(), $this->get45(), $this->get46(), $this->get47()); + $object = new \DrupalCodeBuilder\Task\Collect\PluginTypesCollector($this->get45(), $this->get46(), $this->get47(), $this->get48()); return $object; } - protected function get49() + protected function get50() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get50() + protected function get51() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\ContainerBuilderGetter'); } - protected function get51() + protected function get52() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\MethodCollector'); } - protected function get48() + protected function get49() { - $object = new \DrupalCodeBuilder\Task\Collect\ServiceTagTypesCollector($this->get49(), $this->get50(), $this->get51()); + $object = new \DrupalCodeBuilder\Task\Collect\ServiceTagTypesCollector($this->get50(), $this->get51(), $this->get52()); return $object; } - protected function get53() + protected function get54() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get54() + protected function get55() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\ContainerBuilderGetter'); } - protected function get55() + protected function get56() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\CodeAnalyser'); } - protected function get52() + protected function get53() { - $object = new \DrupalCodeBuilder\Task\Collect\ServicesCollector($this->get53(), $this->get54(), $this->get55()); + $object = new \DrupalCodeBuilder\Task\Collect\ServicesCollector($this->get54(), $this->get55(), $this->get56()); return $object; } - protected function get57() + protected function get58() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get58() + protected function get59() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\HooksCollector'); } - protected function get56() + protected function get57() { - $object = new \DrupalCodeBuilder\Task\Collect5($this->get57(), $this->get58()); + $object = new \DrupalCodeBuilder\Task\Collect5($this->get58(), $this->get59()); return $object; } - protected function get60() + protected function get61() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get61() + protected function get62() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\HooksCollector'); } - protected function get59() + protected function get60() { - $object = new \DrupalCodeBuilder\Task\Collect6($this->get60(), $this->get61()); + $object = new \DrupalCodeBuilder\Task\Collect6($this->get61(), $this->get62()); return $object; } - protected function get63() + protected function get64() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get64() + protected function get65() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\HooksCollector'); } - protected function get62() + protected function get63() { - $object = new \DrupalCodeBuilder\Task\Collect7($this->get63(), $this->get64()); + $object = new \DrupalCodeBuilder\Task\Collect7($this->get64(), $this->get65()); return $object; } - protected function get66() + protected function get67() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get67() + protected function get68() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Generate\\ComponentClassHandler'); } - protected function get65() + protected function get66() { - $object = new \DrupalCodeBuilder\Task\Configuration($this->get66(), $this->get67()); + $object = new \DrupalCodeBuilder\Task\Configuration($this->get67(), $this->get68()); return $object; } - protected function get69() + protected function get70() + { + return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); + } + + protected function get71() { return $this->delegateContainer->get('generator_classmap'); } - protected function get68() + protected function get69() { - $object = new \DrupalCodeBuilder\Task\Generate\ComponentClassHandler($this->get69()); + $object = new \DrupalCodeBuilder\Task\Generate\ComponentClassHandler($this->get70(), $this->get71()); return $object; } - protected function get71() + protected function get73() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get72() + protected function get74() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Generate\\ComponentClassHandler'); } - protected function get70() - { - $object = new \DrupalCodeBuilder\Task\Generate\ComponentCollector($this->get71(), $this->get72()); - return $object; - } - - protected function get73() + protected function get72() { - $object = new \DrupalCodeBuilder\Task\Generate\FileAssembler(); + $object = new \DrupalCodeBuilder\Task\Generate\ComponentCollector($this->get73(), $this->get74()); return $object; } protected function get75() { - return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); - } - - protected function get74() - { - $object = new \DrupalCodeBuilder\Task\ReportAdminRoutes($this->get75()); + $object = new \DrupalCodeBuilder\Task\Generate\FileAssembler(); return $object; } @@ -665,7 +680,7 @@ protected function get77() protected function get76() { - $object = new \DrupalCodeBuilder\Task\ReportDataTypes($this->get77()); + $object = new \DrupalCodeBuilder\Task\ReportAdminRoutes($this->get77()); return $object; } @@ -676,7 +691,7 @@ protected function get79() protected function get78() { - $object = new \DrupalCodeBuilder\Task\ReportElementTypes($this->get79()); + $object = new \DrupalCodeBuilder\Task\ReportDataTypes($this->get79()); return $object; } @@ -687,7 +702,7 @@ protected function get81() protected function get80() { - $object = new \DrupalCodeBuilder\Task\ReportEntityTypes($this->get81()); + $object = new \DrupalCodeBuilder\Task\ReportElementTypes($this->get81()); return $object; } @@ -698,7 +713,7 @@ protected function get83() protected function get82() { - $object = new \DrupalCodeBuilder\Task\ReportEventNames($this->get83()); + $object = new \DrupalCodeBuilder\Task\ReportEntityTypes($this->get83()); return $object; } @@ -709,7 +724,7 @@ protected function get85() protected function get84() { - $object = new \DrupalCodeBuilder\Task\ReportFieldTypes($this->get85()); + $object = new \DrupalCodeBuilder\Task\ReportEventNames($this->get85()); return $object; } @@ -720,7 +735,7 @@ protected function get87() protected function get86() { - $object = new \DrupalCodeBuilder\Task\ReportHookData($this->get87()); + $object = new \DrupalCodeBuilder\Task\ReportFieldTypes($this->get87()); return $object; } @@ -731,18 +746,18 @@ protected function get89() protected function get88() { - $object = new \DrupalCodeBuilder\Task\ReportHookDataFolder($this->get89()); + $object = new \DrupalCodeBuilder\Task\ReportHookClassMethodData($this->get89()); return $object; } protected function get91() { - return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\ReportHookData'); + return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } protected function get90() { - $object = new \DrupalCodeBuilder\Task\ReportHookGroups($this->get91()); + $object = new \DrupalCodeBuilder\Task\ReportHookData($this->get91()); return $object; } @@ -753,18 +768,18 @@ protected function get93() protected function get92() { - $object = new \DrupalCodeBuilder\Task\ReportHookPresets($this->get93()); + $object = new \DrupalCodeBuilder\Task\ReportHookDataFolder($this->get93()); return $object; } protected function get95() { - return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); + return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\ReportHookData'); } protected function get94() { - $object = new \DrupalCodeBuilder\Task\ReportPluginData($this->get95()); + $object = new \DrupalCodeBuilder\Task\ReportHookGroups($this->get95()); return $object; } @@ -775,7 +790,7 @@ protected function get97() protected function get96() { - $object = new \DrupalCodeBuilder\Task\ReportServiceData($this->get97()); + $object = new \DrupalCodeBuilder\Task\ReportHookPresets($this->get97()); return $object; } @@ -786,7 +801,7 @@ protected function get99() protected function get98() { - $object = new \DrupalCodeBuilder\Task\ReportServiceTags($this->get99()); + $object = new \DrupalCodeBuilder\Task\ReportPluginData($this->get99()); return $object; } @@ -795,438 +810,490 @@ protected function get101() return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } + protected function get100() + { + $object = new \DrupalCodeBuilder\Task\ReportServiceData($this->get101()); + return $object; + } + protected function get103() + { + return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); + } + + protected function get102() + { + $object = new \DrupalCodeBuilder\Task\ReportServiceTags($this->get103()); + return $object; + } + + protected function get105() + { + return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); + } + + protected function get107() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get104() + protected function get108() { return $this->delegateContainer->get('ReportAdminRoutes'); } - protected function get105() + protected function get109() { return $this->delegateContainer->get('ReportDataTypes'); } - protected function get106() + protected function get110() { return $this->delegateContainer->get('ReportElementTypes'); } - protected function get107() + protected function get111() { return $this->delegateContainer->get('ReportEntityTypes'); } - protected function get108() + protected function get112() { return $this->delegateContainer->get('ReportEventNames'); } - protected function get109() + protected function get113() { return $this->delegateContainer->get('ReportFieldTypes'); } - protected function get110() + protected function get114() + { + return $this->delegateContainer->get('ReportHookClassMethodData'); + } + + protected function get115() { return $this->delegateContainer->get('ReportHookData'); } - protected function get111() + protected function get116() { return $this->delegateContainer->get('ReportPluginData'); } - protected function get112() + protected function get117() { return $this->delegateContainer->get('ReportServiceData'); } - protected function get113() + protected function get118() { return $this->delegateContainer->get('ReportServiceTags'); } - protected function get102() + protected function get106() { return [ - 'Analyse\\TestTraits' => $this->get103(), - 'ReportAdminRoutes' => $this->get104(), - 'ReportDataTypes' => $this->get105(), - 'ReportElementTypes' => $this->get106(), - 'ReportEntityTypes' => $this->get107(), - 'ReportEventNames' => $this->get108(), - 'ReportFieldTypes' => $this->get109(), - 'ReportHookData' => $this->get110(), - 'ReportPluginData' => $this->get111(), - 'ReportServiceData' => $this->get112(), - 'ReportServiceTags' => $this->get113(), + 'Analyse\\TestTraits' => $this->get107(), + 'ReportAdminRoutes' => $this->get108(), + 'ReportDataTypes' => $this->get109(), + 'ReportElementTypes' => $this->get110(), + 'ReportEntityTypes' => $this->get111(), + 'ReportEventNames' => $this->get112(), + 'ReportFieldTypes' => $this->get113(), + 'ReportHookClassMethodData' => $this->get114(), + 'ReportHookData' => $this->get115(), + 'ReportPluginData' => $this->get116(), + 'ReportServiceData' => $this->get117(), + 'ReportServiceTags' => $this->get118(), ]; } - protected function get100() + protected function get104() { - $object = new \DrupalCodeBuilder\Task\ReportSummary($this->get101()); - $object->setReportHelpers($this->get102()); + $object = new \DrupalCodeBuilder\Task\ReportSummary($this->get105()); + $object->setReportHelpers($this->get106()); return $object; } - protected function get115() + protected function get120() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get117() + protected function get122() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get118() + protected function get123() { return $this->delegateContainer->get('Collect\\AdminRoutesCollector'); } - protected function get119() + protected function get124() { return $this->delegateContainer->get('Collect\\DataTypesCollector'); } - protected function get120() + protected function get125() { return $this->delegateContainer->get('Collect\\ElementTypesCollector'); } - protected function get121() + protected function get126() { return $this->delegateContainer->get('Collect\\EntityTypesCollector'); } - protected function get122() + protected function get127() { return $this->delegateContainer->get('Collect\\EventNamesCollector'); } - protected function get123() + protected function get128() { return $this->delegateContainer->get('Collect\\FieldTypesCollector'); } - protected function get124() + protected function get129() + { + return $this->delegateContainer->get('Collect\\MetadataCollector'); + } + + protected function get130() { return $this->delegateContainer->get('Collect\\PluginTypesCollector'); } - protected function get125() + protected function get131() { return $this->delegateContainer->get('Collect\\ServiceTagTypesCollector'); } - protected function get126() + protected function get132() { return $this->delegateContainer->get('Collect\\ServicesCollector'); } - protected function get127() + protected function get133() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get116() + protected function get121() { return [ - 'Analyse\\TestTraits' => $this->get117(), - 'Collect\\AdminRoutesCollector' => $this->get118(), - 'Collect\\DataTypesCollector' => $this->get119(), - 'Collect\\ElementTypesCollector' => $this->get120(), - 'Collect\\EntityTypesCollector' => $this->get121(), - 'Collect\\EventNamesCollector' => $this->get122(), - 'Collect\\FieldTypesCollector' => $this->get123(), - 'Collect\\PluginTypesCollector' => $this->get124(), - 'Collect\\ServiceTagTypesCollector' => $this->get125(), - 'Collect\\ServicesCollector' => $this->get126(), - 'Collect\\HooksCollector' => $this->get127(), + 'Analyse\\TestTraits' => $this->get122(), + 'Collect\\AdminRoutesCollector' => $this->get123(), + 'Collect\\DataTypesCollector' => $this->get124(), + 'Collect\\ElementTypesCollector' => $this->get125(), + 'Collect\\EntityTypesCollector' => $this->get126(), + 'Collect\\EventNamesCollector' => $this->get127(), + 'Collect\\FieldTypesCollector' => $this->get128(), + 'Collect\\MetadataCollector' => $this->get129(), + 'Collect\\PluginTypesCollector' => $this->get130(), + 'Collect\\ServiceTagTypesCollector' => $this->get131(), + 'Collect\\ServicesCollector' => $this->get132(), + 'Collect\\HooksCollector' => $this->get133(), ]; } - protected function get114() + protected function get119() { - $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting10($this->get115()); - $object->setCollectors($this->get116()); + $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting10($this->get120()); + $object->setCollectors($this->get121()); return $object; } - protected function get129() + protected function get135() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get131() + protected function get137() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get132() + protected function get138() { return $this->delegateContainer->get('Collect\\AdminRoutesCollector'); } - protected function get133() + protected function get139() { return $this->delegateContainer->get('Collect\\DataTypesCollector'); } - protected function get134() + protected function get140() { return $this->delegateContainer->get('Collect\\ElementTypesCollector'); } - protected function get135() + protected function get141() { return $this->delegateContainer->get('Collect\\EntityTypesCollector'); } - protected function get136() + protected function get142() { return $this->delegateContainer->get('Collect\\EventNamesCollector'); } - protected function get137() + protected function get143() { return $this->delegateContainer->get('Collect\\FieldTypesCollector'); } - protected function get138() + protected function get144() + { + return $this->delegateContainer->get('Collect\\MetadataCollector'); + } + + protected function get145() { return $this->delegateContainer->get('Collect\\PluginTypesCollector'); } - protected function get139() + protected function get146() { return $this->delegateContainer->get('Collect\\ServiceTagTypesCollector'); } - protected function get140() + protected function get147() { return $this->delegateContainer->get('Collect\\ServicesCollector'); } - protected function get141() + protected function get148() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get130() + protected function get136() { return [ - 'Analyse\\TestTraits' => $this->get131(), - 'Collect\\AdminRoutesCollector' => $this->get132(), - 'Collect\\DataTypesCollector' => $this->get133(), - 'Collect\\ElementTypesCollector' => $this->get134(), - 'Collect\\EntityTypesCollector' => $this->get135(), - 'Collect\\EventNamesCollector' => $this->get136(), - 'Collect\\FieldTypesCollector' => $this->get137(), - 'Collect\\PluginTypesCollector' => $this->get138(), - 'Collect\\ServiceTagTypesCollector' => $this->get139(), - 'Collect\\ServicesCollector' => $this->get140(), - 'Collect\\HooksCollector' => $this->get141(), + 'Analyse\\TestTraits' => $this->get137(), + 'Collect\\AdminRoutesCollector' => $this->get138(), + 'Collect\\DataTypesCollector' => $this->get139(), + 'Collect\\ElementTypesCollector' => $this->get140(), + 'Collect\\EntityTypesCollector' => $this->get141(), + 'Collect\\EventNamesCollector' => $this->get142(), + 'Collect\\FieldTypesCollector' => $this->get143(), + 'Collect\\MetadataCollector' => $this->get144(), + 'Collect\\PluginTypesCollector' => $this->get145(), + 'Collect\\ServiceTagTypesCollector' => $this->get146(), + 'Collect\\ServicesCollector' => $this->get147(), + 'Collect\\HooksCollector' => $this->get148(), ]; } - protected function get128() + protected function get134() { - $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting11($this->get129()); - $object->setCollectors($this->get130()); + $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting11($this->get135()); + $object->setCollectors($this->get136()); return $object; } - protected function get143() + protected function get150() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get144() + protected function get151() { return $this->delegateContainer->get('DrupalCodeBuilder\\Task\\Collect\\HooksCollector'); } - protected function get142() + protected function get149() { - $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting7($this->get143(), $this->get144()); + $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting7($this->get150(), $this->get151()); return $object; } - protected function get146() + protected function get153() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get148() + protected function get155() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get149() + protected function get156() { return $this->delegateContainer->get('Collect\\AdminRoutesCollector'); } - protected function get150() + protected function get157() { return $this->delegateContainer->get('Collect\\DataTypesCollector'); } - protected function get151() + protected function get158() { return $this->delegateContainer->get('Collect\\ElementTypesCollector'); } - protected function get152() + protected function get159() { return $this->delegateContainer->get('Collect\\EntityTypesCollector'); } - protected function get153() + protected function get160() { return $this->delegateContainer->get('Collect\\EventNamesCollector'); } - protected function get154() + protected function get161() { return $this->delegateContainer->get('Collect\\FieldTypesCollector'); } - protected function get155() + protected function get162() + { + return $this->delegateContainer->get('Collect\\MetadataCollector'); + } + + protected function get163() { return $this->delegateContainer->get('Collect\\PluginTypesCollector'); } - protected function get156() + protected function get164() { return $this->delegateContainer->get('Collect\\ServiceTagTypesCollector'); } - protected function get157() + protected function get165() { return $this->delegateContainer->get('Collect\\ServicesCollector'); } - protected function get158() + protected function get166() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get147() + protected function get154() { return [ - 'Analyse\\TestTraits' => $this->get148(), - 'Collect\\AdminRoutesCollector' => $this->get149(), - 'Collect\\DataTypesCollector' => $this->get150(), - 'Collect\\ElementTypesCollector' => $this->get151(), - 'Collect\\EntityTypesCollector' => $this->get152(), - 'Collect\\EventNamesCollector' => $this->get153(), - 'Collect\\FieldTypesCollector' => $this->get154(), - 'Collect\\PluginTypesCollector' => $this->get155(), - 'Collect\\ServiceTagTypesCollector' => $this->get156(), - 'Collect\\ServicesCollector' => $this->get157(), - 'Collect\\HooksCollector' => $this->get158(), + 'Analyse\\TestTraits' => $this->get155(), + 'Collect\\AdminRoutesCollector' => $this->get156(), + 'Collect\\DataTypesCollector' => $this->get157(), + 'Collect\\ElementTypesCollector' => $this->get158(), + 'Collect\\EntityTypesCollector' => $this->get159(), + 'Collect\\EventNamesCollector' => $this->get160(), + 'Collect\\FieldTypesCollector' => $this->get161(), + 'Collect\\MetadataCollector' => $this->get162(), + 'Collect\\PluginTypesCollector' => $this->get163(), + 'Collect\\ServiceTagTypesCollector' => $this->get164(), + 'Collect\\ServicesCollector' => $this->get165(), + 'Collect\\HooksCollector' => $this->get166(), ]; } - protected function get145() + protected function get152() { - $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting8($this->get146()); - $object->setCollectors($this->get147()); + $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting8($this->get153()); + $object->setCollectors($this->get154()); return $object; } - protected function get160() + protected function get168() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get162() + protected function get170() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get163() + protected function get171() { return $this->delegateContainer->get('Collect\\AdminRoutesCollector'); } - protected function get164() + protected function get172() { return $this->delegateContainer->get('Collect\\DataTypesCollector'); } - protected function get165() + protected function get173() { return $this->delegateContainer->get('Collect\\ElementTypesCollector'); } - protected function get166() + protected function get174() { return $this->delegateContainer->get('Collect\\EntityTypesCollector'); } - protected function get167() + protected function get175() { return $this->delegateContainer->get('Collect\\EventNamesCollector'); } - protected function get168() + protected function get176() { return $this->delegateContainer->get('Collect\\FieldTypesCollector'); } - protected function get169() + protected function get177() + { + return $this->delegateContainer->get('Collect\\MetadataCollector'); + } + + protected function get178() { return $this->delegateContainer->get('Collect\\PluginTypesCollector'); } - protected function get170() + protected function get179() { return $this->delegateContainer->get('Collect\\ServiceTagTypesCollector'); } - protected function get171() + protected function get180() { return $this->delegateContainer->get('Collect\\ServicesCollector'); } - protected function get172() + protected function get181() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get161() + protected function get169() { return [ - 'Analyse\\TestTraits' => $this->get162(), - 'Collect\\AdminRoutesCollector' => $this->get163(), - 'Collect\\DataTypesCollector' => $this->get164(), - 'Collect\\ElementTypesCollector' => $this->get165(), - 'Collect\\EntityTypesCollector' => $this->get166(), - 'Collect\\EventNamesCollector' => $this->get167(), - 'Collect\\FieldTypesCollector' => $this->get168(), - 'Collect\\PluginTypesCollector' => $this->get169(), - 'Collect\\ServiceTagTypesCollector' => $this->get170(), - 'Collect\\ServicesCollector' => $this->get171(), - 'Collect\\HooksCollector' => $this->get172(), + 'Analyse\\TestTraits' => $this->get170(), + 'Collect\\AdminRoutesCollector' => $this->get171(), + 'Collect\\DataTypesCollector' => $this->get172(), + 'Collect\\ElementTypesCollector' => $this->get173(), + 'Collect\\EntityTypesCollector' => $this->get174(), + 'Collect\\EventNamesCollector' => $this->get175(), + 'Collect\\FieldTypesCollector' => $this->get176(), + 'Collect\\MetadataCollector' => $this->get177(), + 'Collect\\PluginTypesCollector' => $this->get178(), + 'Collect\\ServiceTagTypesCollector' => $this->get179(), + 'Collect\\ServicesCollector' => $this->get180(), + 'Collect\\HooksCollector' => $this->get181(), ]; } - protected function get159() + protected function get167() { - $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting9($this->get160()); - $object->setCollectors($this->get161()); + $object = new \DrupalCodeBuilder\Task\Testing\CollectTesting9($this->get168()); + $object->setCollectors($this->get169()); return $object; } - protected function get173() + protected function get182() { return $this->resolveFactory([ 0 => 'DrupalCodeBuilder\\DependencyInjection\\ServiceFactories', @@ -1236,7 +1303,7 @@ protected function get173() ]); } - protected function get174() + protected function get183() { return $this->resolveFactory([ 0 => 'DrupalCodeBuilder\\DependencyInjection\\ServiceFactories', @@ -1246,7 +1313,7 @@ protected function get174() ]); } - protected function get175() + protected function get184() { return $this->resolveFactory([ 0 => 'DrupalCodeBuilder\\DependencyInjection\\ServiceFactories', @@ -1254,7 +1321,7 @@ protected function get175() ], 'Collect\\HooksCollector'); } - protected function get176() + protected function get185() { return $this->resolveFactory([ 0 => 'DrupalCodeBuilder\\DependencyInjection\\ServiceFactories', @@ -1262,91 +1329,97 @@ protected function get176() ], 'Collect'); } - protected function get178() + protected function get187() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get180() + protected function get189() { return $this->delegateContainer->get('Analyse\\TestTraits'); } - protected function get181() + protected function get190() { return $this->delegateContainer->get('Collect\\AdminRoutesCollector'); } - protected function get182() + protected function get191() { return $this->delegateContainer->get('Collect\\DataTypesCollector'); } - protected function get183() + protected function get192() { return $this->delegateContainer->get('Collect\\ElementTypesCollector'); } - protected function get184() + protected function get193() { return $this->delegateContainer->get('Collect\\EntityTypesCollector'); } - protected function get185() + protected function get194() { return $this->delegateContainer->get('Collect\\EventNamesCollector'); } - protected function get186() + protected function get195() { return $this->delegateContainer->get('Collect\\FieldTypesCollector'); } - protected function get187() + protected function get196() + { + return $this->delegateContainer->get('Collect\\MetadataCollector'); + } + + protected function get197() { return $this->delegateContainer->get('Collect\\PluginTypesCollector'); } - protected function get188() + protected function get198() { return $this->delegateContainer->get('Collect\\ServiceTagTypesCollector'); } - protected function get189() + protected function get199() { return $this->delegateContainer->get('Collect\\ServicesCollector'); } - protected function get190() + protected function get200() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get179() + protected function get188() { return [ - 'Analyse\\TestTraits' => $this->get180(), - 'Collect\\AdminRoutesCollector' => $this->get181(), - 'Collect\\DataTypesCollector' => $this->get182(), - 'Collect\\ElementTypesCollector' => $this->get183(), - 'Collect\\EntityTypesCollector' => $this->get184(), - 'Collect\\EventNamesCollector' => $this->get185(), - 'Collect\\FieldTypesCollector' => $this->get186(), - 'Collect\\PluginTypesCollector' => $this->get187(), - 'Collect\\ServiceTagTypesCollector' => $this->get188(), - 'Collect\\ServicesCollector' => $this->get189(), - 'Collect\\HooksCollector' => $this->get190(), + 'Analyse\\TestTraits' => $this->get189(), + 'Collect\\AdminRoutesCollector' => $this->get190(), + 'Collect\\DataTypesCollector' => $this->get191(), + 'Collect\\ElementTypesCollector' => $this->get192(), + 'Collect\\EntityTypesCollector' => $this->get193(), + 'Collect\\EventNamesCollector' => $this->get194(), + 'Collect\\FieldTypesCollector' => $this->get195(), + 'Collect\\MetadataCollector' => $this->get196(), + 'Collect\\PluginTypesCollector' => $this->get197(), + 'Collect\\ServiceTagTypesCollector' => $this->get198(), + 'Collect\\ServicesCollector' => $this->get199(), + 'Collect\\HooksCollector' => $this->get200(), ]; } - protected function get177() + protected function get186() { - $object = new \DrupalCodeBuilder\Task\Collect($this->get178()); - $object->setCollectors($this->get179()); + $object = new \DrupalCodeBuilder\Task\Collect($this->get187()); + $object->setCollectors($this->get188()); return $object; } - protected function get191() + protected function get201() { return $this->resolveFactory([ 0 => 'DrupalCodeBuilder\\DependencyInjection\\ServiceFactories', @@ -1354,53 +1427,58 @@ protected function get191() ], 'Testing\\CollectTesting'); } - protected function get192() + protected function get202() { return $this->delegateContainer->get('Collect\\HooksCollector'); } - protected function get194() + protected function get204() + { + return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); + } + + protected function get205() { return $this->delegateContainer->get('generator_classmap'); } - protected function get193() + protected function get203() { - $object = new DrupalCodeBuilder\Task\Generate\ComponentClassHandler($this->get194()); + $object = new DrupalCodeBuilder\Task\Generate\ComponentClassHandler($this->get204(), $this->get205()); return $object; } - protected function get195() + protected function get206() { $object = new DrupalCodeBuilder\Task\Collect\ContainerBuilderGetter(); return $object; } - protected function get196() + protected function get207() { $object = new DrupalCodeBuilder\Task\Collect\MethodCollector(); return $object; } - protected function get198() + protected function get209() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get197() + protected function get208() { - $object = new DrupalCodeBuilder\Task\Collect\CodeAnalyser($this->get198()); + $object = new DrupalCodeBuilder\Task\Collect\CodeAnalyser($this->get209()); return $object; } - protected function get200() + protected function get211() { return $this->delegateContainer->get('DrupalCodeBuilder\\Environment\\EnvironmentInterface'); } - protected function get199() + protected function get210() { - $object = new DrupalCodeBuilder\Task\ReportHookData($this->get200()); + $object = new DrupalCodeBuilder\Task\ReportHookData($this->get211()); return $object; } diff --git a/Environment/EnvironmentInterface.php b/Environment/EnvironmentInterface.php index e42b78da..0f28d678 100644 --- a/Environment/EnvironmentInterface.php +++ b/Environment/EnvironmentInterface.php @@ -216,10 +216,13 @@ public function getExtensionPath($type, $name); public function getContainer(); /** - * Gets the Drupal root. + * Gets the Drupal application root. + * + * This is the web root where Drupal is installed, rather than the Composer + * project root. * * @return string - * The root path. + * The absolute root path. */ public function getRoot(); diff --git a/File/CodeFile.php b/File/CodeFile.php index 78bea945..8d7e581d 100644 --- a/File/CodeFile.php +++ b/File/CodeFile.php @@ -13,7 +13,7 @@ * This goes through a refinement process, with some properties being set later * and some which hold earlier versions of data being unset. */ -class CodeFile implements \Stringable, CodeFileInterface { +class CodeFile implements CodeFileInterface { /** * The array of body pieces. @@ -165,10 +165,6 @@ public function fileIsMerged(): bool { /** * Gets the relative filepath. * - * @internal - * - * @todo Make this part of the API in 4.3.0. - * * @return string * The filepath. */ diff --git a/File/CodeFileInterface.php b/File/CodeFileInterface.php index c2aa2f69..e022ea75 100644 --- a/File/CodeFileInterface.php +++ b/File/CodeFileInterface.php @@ -8,7 +8,7 @@ * This may include the code of file on disk if this exists and the file is of * a type that we are able to merge. */ -interface CodeFileInterface { +interface CodeFileInterface extends \Stringable { /** * Gets the code for the file. diff --git a/File/DrupalExtension.php b/File/DrupalExtension.php index b06d7d68..fd2d32f1 100644 --- a/File/DrupalExtension.php +++ b/File/DrupalExtension.php @@ -18,28 +18,18 @@ class DrupalExtension { /** * The extension type, e.g. 'module'. - * - * TODO Make readonly in PHP 8.1. - * - * @var string */ - public $type; + public readonly string $type; /** * The extension name. - * - * TODO Make readonly in PHP 8.1. - * - * @var string */ - public $name; + public readonly string $name; /** * The given extension path. - * - * @var string */ - protected $path; + protected readonly string $path; /** * Constructs a new extension. @@ -251,4 +241,36 @@ protected function getFileContents($relative_file_path) { return file_get_contents($this->getRealPath($relative_file_path)); } + /** + * Loads the file for a class from this extension. + * + * For extensions which are not currently enabled, Drupal's autoloader will + * not be able to find the class. This will load the class even if the + * extension is not enabled. + * + * @param string $class_name + * The fully-qualified class name. + * + * @internal + */ + public function loadClass(string $class_name): void { + // Trim the class name up to the extension name piece. + $relative_class_name = preg_replace("@Drupal\\\\{$this->name}\\\\@", '', $class_name); + $relative_path = 'src/' . str_replace('\\', '/', $relative_class_name) . '.php'; + + $this->includeFile($relative_path); + } + + /** + * Includes a file from this extension. + * + * @param string $relative_file_path + * The filepath relative to the extension folder. + * + * @internal + */ + public function includeFile(string $relative_file_path): void { + include_once $this->getRealPath($relative_file_path); + } + } diff --git a/Generator/AdminSettingsForm.php b/Generator/AdminSettingsForm.php index 834582be..11f14253 100644 --- a/Generator/AdminSettingsForm.php +++ b/Generator/AdminSettingsForm.php @@ -6,6 +6,7 @@ use MutableTypedData\Definition\DefaultDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\File\DrupalExtension; +use DrupalCodeBuilder\Utility\InsertArray; use MutableTypedData\Definition\OptionsSortOrder; /** @@ -86,6 +87,15 @@ public static function findAdoptableComponents(DrupalExtension $extension): arra public function requiredComponents(): array { $components = parent::requiredComponents(); + InsertArray::insertBefore($components, 'buildForm', ['getEditableConfigNames' => [ + 'component_type' => 'PHPFunction', + 'function_name' => 'getEditableConfigNames', + 'containing_component' => '%requester', + 'docblock_inherit' => TRUE, + 'declaration' => 'protected function getEditableConfigNames()', + 'body' => "return ['%module.settings'];", + ]]); + // Restore the call to the parent method. $components['buildForm']['body'] = [ "£form = parent::buildForm(£form, £form_state);", @@ -115,15 +125,6 @@ public function requiredComponents(): array { '£config->save();', ]; - $components['getEditableConfigNames'] = [ - 'component_type' => 'PHPFunction', - 'function_name' => 'getEditableConfigNames', - 'containing_component' => '%requester', - 'docblock_inherit' => TRUE, - 'declaration' => 'protected function getEditableConfigNames()', - 'body' => "return ['%module.settings'];", - ]; - $task_handler_report_admin_routes = \DrupalCodeBuilder\Factory::getTask('ReportAdminRoutes'); $admin_routes = $task_handler_report_admin_routes->listAdminRoutes(); @@ -138,7 +139,7 @@ public function requiredComponents(): array { // OK to use a token here, as the YAML value for this will be quoted // anyway. 'path' => $settings_form_path, - 'title' => 'Administer %lower', + 'title' => '%Module settings' , 'controller' => [ 'controller_type' => 'form', 'routing_value' => '\\' . $this->component_data['qualified_class_name'], diff --git a/Generator/AdoptableInterface.php b/Generator/AdoptableInterface.php index 10c5cbc9..76ca7522 100644 --- a/Generator/AdoptableInterface.php +++ b/Generator/AdoptableInterface.php @@ -27,10 +27,11 @@ interface AdoptableInterface { public static function findAdoptableComponents(DrupalExtension $extension): array; /** - * Adopt a component from an existing extension. + * Adopts a component from an existing extension. * * @param \MutableTypedData\Data\DataItem $component_data - * The existing root component data. + * The existing root component data. Values should be added to this for the + * component being adopted. * @param \DrupalCodeBuilder\File\DrupalExtension $extension * The existing extension to analyse. * @param string $property_name diff --git a/Generator/BaseGenerator.php b/Generator/BaseGenerator.php index 73012062..51b67d01 100644 --- a/Generator/BaseGenerator.php +++ b/Generator/BaseGenerator.php @@ -2,12 +2,12 @@ namespace DrupalCodeBuilder\Generator; +use DrupalCodeBuilder\Attribute\RelatedBaseClass; use MutableTypedData\Definition\PropertyListInterface; use DrupalCodeBuilder\Generator\Collection\ComponentCollection; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Exception\MergeDataLossException; use DrupalCodeBuilder\File\DrupalExtension; -use DrupalCodeBuilder\Task\Generate\ComponentClassHandler; use MutableTypedData\Data\DataItem; /** @@ -160,13 +160,6 @@ abstract class BaseGenerator implements GeneratorInterface { */ protected $containedComponents = []; - /** - * The class handler. - * - * @var \DrupalCodeBuilder\Task\Generate\ComponentClassHandler - */ - protected $classHandler; - /** * The existing extension, if applicable. * @@ -202,19 +195,16 @@ function __construct(DataItem $component_data) { // Set the type. This is the short class name without the numeric version // suffix. $class = get_class($this); - $class_pieces = explode('\\', $class); - $short_class = array_pop($class_pieces); - $this->type = preg_replace('@\d+$@', '', $short_class); - } - /** - * Sets the class handler. - * - * @param \DrupalCodeBuilder\Task\Generate\ComponentClassHandler $class_handler - * The class handler. - */ - public function setClassHandler(ComponentClassHandler $class_handler) { - $this->classHandler = $class_handler; + $reflector = new \ReflectionClass($class); + if ($base_class_attributes = $reflector->getAttributes(RelatedBaseClass::class)) { + $this->type = $base_class_attributes[0]->newInstance()->base_class; + } + else { + $class_pieces = explode('\\', $class); + $short_class = array_pop($class_pieces); + $this->type = preg_replace('@\d+$@', '', $short_class); + } } /** @@ -273,6 +263,26 @@ final public static function addBasePropertiesToPropertyDefinition(PropertyListI ]); } + /** + * Gets a diffentiated label suffix for a delta item. + * + * This allows different delta items to differentiate their labels in the UI + * with more than just a delta index. + * + * This may be called for incomplete data items (such as those added into a + * form) and so must account for even required data not being present. + * + * @param \MutableTypedData\Data\DataItem $data + * The data item to get a label suffix for. + * + * @return string|null + * The label suffix, or NULL if useful data is available. It does not need + * any initial space or separator. + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return NULL; + } + public function isRootComponent(): bool { return FALSE; } diff --git a/Generator/ClassHandlerAware.php b/Generator/ClassHandlerAware.php new file mode 100644 index 00000000..6964c675 --- /dev/null +++ b/Generator/ClassHandlerAware.php @@ -0,0 +1,26 @@ +classHandler = $class_handler; + } + +} diff --git a/Generator/ConfigEntityType.php b/Generator/ConfigEntityType.php index 59c00789..66274106 100644 --- a/Generator/ConfigEntityType.php +++ b/Generator/ConfigEntityType.php @@ -112,7 +112,7 @@ protected static function getHandlerTypes() { $handler_types[$form_handler_type]['base_class'] = '\Drupal\Core\Entity\EntityForm'; $handler_types[$form_handler_type]['handler_properties'] = [ - // Config entity formss redirect to the collection page. + // Config entity forms redirect to the collection page. 'redirect_link_template' => 'collection', ]; } diff --git a/Generator/ContentEntityType.php b/Generator/ContentEntityType.php index 609e2e60..00aa42cf 100644 --- a/Generator/ContentEntityType.php +++ b/Generator/ContentEntityType.php @@ -41,6 +41,7 @@ class ContentEntityType extends EntityTypeBase { 'handlers', 'admin_permission', 'entity_keys', + 'revision_metadata_keys', 'bundle_entity_type', 'field_ui_base_route', 'links', @@ -205,7 +206,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio return 'entity.' . $entity_data->bundle_entity_type_id->value . '.edit_form'; } else { - return 'entity.' . $entity_data->entity_type_id->value . '.admin_form'; + return 'entity.' . $entity_data->entity_type_id->value . '.settings'; } }) ->setDependencies('..:functionality') @@ -520,6 +521,37 @@ public function requiredComponents(): array { } } + // Add the Field UI base route definition and controller if there is no + // bundle entity type to provide a bundle entity collection as the Field UI + // base route. + if ($this->component_data->field_ui_base_route->value && $this->component_data->bundle_entity->isEmpty()) { + $components['field_ui_base_route'] = [ + 'component_type' => 'RouterItem', + 'route_name' => $this->component_data->field_ui_base_route->value, + 'path' => '/admin/structure/' . $this->component_data->entity_type_id->value, + 'title' => $this->component_data->entity_type_label->value . ' settings', + 'controller' => [ + 'controller_type' => 'controller', + 'use_base' => TRUE, + ], + 'access' => [ + 'access_type' => 'permission', + 'routing_value' => $this->component_data->admin_permission_name->value, + ], + ]; + + // This will merge with the controller requested by the RouterItem. + $components['settings_controller'] = [ + 'component_type' => 'Controller', + 'relative_class_name' => RouterItem::controllerRelativeClassFromRoutePath($components['field_ui_base_route']['path']), + 'class_docblock_lines' => [ + 'Controller class for the FieldUI base route.', + "This needs to exist for FieldUI to attach its routes to. If the entity type has general settings, this route can be the config form for them instead.", + "You can also get rid of this route and use https://www.drupal.org/project/entity_admin_handlers to provide it automatically.", + ], + ]; + } + return $components; } @@ -529,13 +561,18 @@ public function requiredComponents(): array { protected function getAnnotationData() { $annotation_data = parent::getAnnotationData(); + $revisionable = in_array('revisionable', $this->component_data['functionality']); + $translatable = in_array('translatable', $this->component_data['functionality']); + $ui = !empty($this->component_data['entity_ui']); + // Add further annotation properties. // Use the entity type ID as the base table. $annotation_data['base_table'] = $this->component_data['entity_type_id']; - if (!empty($this->component_data['entity_ui'])) { + if ($ui) { $annotation_data['links'] = []; $entity_path_component = $this->component_data['entity_type_id']; + $entity_path_placeholder = "{{$entity_path_component}}"; // The structure of the add UI depends on whether there is a bundle // entity. @@ -553,12 +590,10 @@ protected function getAnnotationData() { $annotation_data['links']["add-form"] = "/$entity_path_component/add"; } - $annotation_data['links']["canonical"] = "/$entity_path_component/{{$entity_path_component}}"; + $annotation_data['links']["canonical"] = "/$entity_path_component/$entity_path_placeholder"; $annotation_data['links']["collection"] = "/admin/content/$entity_path_component"; - $annotation_data['links']["delete-form"] = "/$entity_path_component/{{$entity_path_component}}/delete"; - $annotation_data['links']["edit-form"] = "/$entity_path_component/{{$entity_path_component}}/edit"; - // TODO: revision link template. - // $annotation_data['links']["revision"] = "/$entity_path_component/{}/revisions/{media_revision}/view"; + $annotation_data['links']["delete-form"] = "/$entity_path_component/$entity_path_placeholder/delete"; + $annotation_data['links']["edit-form"] = "/$entity_path_component/$entity_path_placeholder/edit"; } if (!$this->component_data->bundle_entity->isEmpty()) { @@ -566,15 +601,18 @@ protected function getAnnotationData() { $annotation_data['bundle_label'] = ClassAnnotation::Translation($this->component_data['bundle_label']); } - $revisionable = in_array('revisionable', $this->component_data['functionality']); - $translatable = in_array('translatable', $this->component_data['functionality']); - if ($this->component_data->field_ui_base_route->value) { $annotation_data['field_ui_base_route'] = $this->component_data->field_ui_base_route->value; } if ($revisionable) { $annotation_data['revision_table'] = "{$annotation_data['base_table']}_revision"; + + $annotation_data['revision_metadata_keys'] = [ + 'revision_user' => 'revision_uid', + 'revision_created' => 'revision_timestamp', + 'revision_log_message' => 'revision_log' + ]; } if ($translatable) { @@ -586,7 +624,34 @@ protected function getAnnotationData() { $annotation_data['revision_data_table'] = "{$annotation_data['base_table']}_field_revision"; } + if ($ui && $revisionable) { + $this->addRevisionUiAnnotationData($annotation_data); + } + return $annotation_data; } + /** + * Adds annotation data for revisions UI. + * + * @param array &$annotation_data + * The annotation data. + */ + protected function addRevisionUiAnnotationData(&$annotation_data) { + $entity_path_component = $this->component_data->entity_type_id->value; + $entity_path_placeholder = "{{$entity_path_component}}"; + $entity_revision_path_placeholder = "{{$entity_path_component}_revision}"; + + $annotation_data['handlers']['route_provider']['revision'] = 'Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider'; + + $annotation_data['handlers']['form']['revision-delete'] = 'Drupal\Core\Entity\Form\RevisionDeleteForm'; + $annotation_data['handlers']['form']['revision-revert'] = 'Drupal\Core\Entity\Form\RevisionRevertForm'; + + $annotation_data['links']['version-history'] = "/$entity_path_component/$entity_path_placeholder/revisions"; + $annotation_data['links']['revision'] = "/$entity_path_component/$entity_path_placeholder/revisions/$entity_revision_path_placeholder/view"; + $annotation_data['links']['revision-delete-form'] = "/$entity_path_component/$entity_path_placeholder/revisions/$entity_revision_path_placeholder/view"; + $annotation_data['links']['revision-revert-form'] = "/$entity_path_component/$entity_path_placeholder/revisions/$entity_revision_path_placeholder/revert"; + $annotation_data['links']['version-history'] = "/$entity_path_component/$entity_path_placeholder/revisions"; + } + } diff --git a/Generator/ContentEntityType9AndLower.php b/Generator/ContentEntityType9AndLower.php new file mode 100644 index 00000000..12a57446 --- /dev/null +++ b/Generator/ContentEntityType9AndLower.php @@ -0,0 +1,24 @@ +setLiteralDefault(TRUE); } - /** - * Produces the class declaration. - */ - function classDeclaration() { - if (isset($this->containedComponents['injected_service'])) { - // Numeric key will clobber, so make something up! - // TODO: fix! - $this->component_data->interfaces->add(['ContainerInjectionInterface' => '\Drupal\Core\DependencyInjection\ContainerInjectionInterface']); - } - - return parent::classDeclaration(); - } - } diff --git a/Generator/DrushCommand.php b/Generator/DrushCommand.php index 40a796d6..11678554 100644 --- a/Generator/DrushCommand.php +++ b/Generator/DrushCommand.php @@ -11,6 +11,7 @@ use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Generator\Render\DocBlock; use DrupalCodeBuilder\Generator\Render\PhpAttributes; +use MutableTypedData\Data\DataItem; use MutableTypedData\Definition\DefaultDefinition; /** @@ -157,6 +158,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLiteralDefault(['public']); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->command_name->value ?: NULL; + } + /** * Return an array of subcomponent types. */ @@ -168,7 +176,7 @@ public function requiredComponents(): array { // Makes this get matched up with the data definition. 'use_data_definition' => TRUE, 'plain_class_name' => CaseString::snake($this->component_data->root_component_name->value)->pascal() . 'Commands', - 'relative_namespace' => 'Commands', + 'relative_namespace' => 'Drush\Commands', 'parent_class_name' => '\Drush\Commands\DrushCommands', 'injected_services' => $this->component_data['injected_services'], 'docblock_first_line' => "%sentence Drush commands.", @@ -283,7 +291,7 @@ protected function getFunctionAttributes(): array { [$option_name, ] = explode(':', $option); $attributes[] = PhpAttributes::method('\Drush\Attributes\Option', [ 'name' => $option_name, - 'description' => "TODO: description of {$parameter} option.", + 'description' => "TODO: description of {$option_name} option.", ]); } @@ -301,7 +309,7 @@ protected function getFunctionAttributes(): array { /** * {@inheritdoc} */ - protected function buildMethodDeclaration($name, $parameters = [], $options = [], string $return_type = NULL): array { + protected function buildMethodDeclaration($name, $parameters = [], $options = [], ?string $return_type = NULL): array { $parameters = []; // Add command parameters. diff --git a/Generator/DrushCommandsClass.php b/Generator/DrushCommandsClass.php index baaa1935..87e7e769 100644 --- a/Generator/DrushCommandsClass.php +++ b/Generator/DrushCommandsClass.php @@ -5,14 +5,19 @@ use MutableTypedData\Definition\PropertyListInterface; /** - * Generator for a class holding Drus commands. + * Generator for a class holding Drush commands. */ class DrushCommandsClass extends PHPClassFileWithInjection { /** * {@inheritdoc} */ - protected string $containerInterface = '\\Psr\\Container\\ContainerInterface'; + protected const CLASS_DI_INTERFACE = NULL; + + /** + * {@inheritdoc} + */ + protected const CONTAINER_INTERFACE = '\\Psr\\Container\\ContainerInterface'; /** * {@inheritdoc} diff --git a/Generator/RouteCallback.php b/Generator/DynamicRouteProvider.php similarity index 51% rename from Generator/RouteCallback.php rename to Generator/DynamicRouteProvider.php index c8519bd2..d4d46d15 100644 --- a/Generator/RouteCallback.php +++ b/Generator/DynamicRouteProvider.php @@ -10,7 +10,7 @@ /** * Generator a dynamic route provider. */ -class RouteCallback extends BaseGenerator { +class DynamicRouteProvider extends PHPClassFileWithInjection { /** * {@inheritdoc} @@ -18,27 +18,19 @@ class RouteCallback extends BaseGenerator { public static function addToGeneratorDefinition(PropertyListInterface $definition) { parent::addToGeneratorDefinition($definition); - $definition->addProperties([ - 'provider_class_short_name' => PropertyDefinition::create('string') - ->setLabel('The short class name of the route provider') - ->setRequired(TRUE) - ->setLiteralDefault('RouteProvider'), - 'provider_qualified_class_name' => PropertyDefinition::create('string') - ->setRequired(TRUE) - ->setInternal(TRUE) - ->setDefault(DefaultDefinition::create() - ->setCallable(function (DataItem $component_data) { - $default = implode('\\', [ - 'Drupal', - $component_data->getParent()->root_component_name->value, - 'Routing', - $component_data->getParent()->provider_class_short_name->value, - ]); - return $default; - }) - ->setDependencies('..:provider_class_short_name') - ), - ]); + $definition->getProperty('relative_namespace') + ->setLiteralDefault('Routing'); + + $definition->getProperty('plain_class_name') + ->setLabel('The short class name of the route provider') + ->setRequired(TRUE) + ->setLiteralDefault('RouteProvider'); + + $definition->getProperty('relative_class_name') + ->setInternal(TRUE); + + $definition->getProperty('use_static_factory_method') + ->setLiteralDefault(TRUE); } /** @@ -51,19 +43,17 @@ public function requiredComponents(): array { // components. $components['%module.routing.yml'] = [ 'component_type' => 'Routing', - ]; - - $components['route_provider'] = [ - 'component_type' => 'PHPClassFile', - 'plain_class_name' => $this->component_data['provider_class_short_name'], - 'relative_namespace' => 'Routing', - 'docblock_first_line' => "Defines dynamic routes.", + 'yaml_data' => [ + 'route_callbacks' => [ + '\\' . $this->component_data->qualified_class_name->value . '::routes', + ], + ], ]; $components["route_provider_method"] = [ 'component_type' => 'PHPFunction', 'function_name' => 'routes', - 'containing_component' => "%requester:route_provider", + 'containing_component' => "%requester", 'prefixes' => ['public'], 'return' => [ 'return_type' => 'array', @@ -93,24 +83,4 @@ public function requiredComponents(): array { return $components; } - /** - * {@inheritdoc} - */ - function containingComponent() { - return '%self:%module.routing.yml'; - } - - /** - * {@inheritdoc} - */ - public function getContents(): array { - $routing_data = [ - 'route_callbacks' => [ - '\\' . $this->component_data['provider_qualified_class_name'] . '::routes', - ], - ]; - - return $routing_data; - } - } diff --git a/Generator/EntityTypeBase.php b/Generator/EntityTypeBase.php index 0e1d64ec..d272792f 100644 --- a/Generator/EntityTypeBase.php +++ b/Generator/EntityTypeBase.php @@ -10,7 +10,7 @@ use CaseConverter\CaseString; use MutableTypedData\Definition\DefaultDefinition; use MutableTypedData\Data\DataItem; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; /** * Base generator entity types. @@ -81,13 +81,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio // - the menu tasks 'entity_ui' => PropertyDefinition::create('string') ->setLabel('Provide UI') - ->setDescription("Whether this entity has a UI. If selected, this will override the route provider, default form, list builder, and admin permission options if they are left empty.") - ->setOptionsArray([ + ->setDescription("Whether this entity has a UI for listing, creating, editing, and viewing the entities. If selected, this will override the route provider, default form, list builder, and admin permission options if they are left empty.") + ->setOptions( // An empty value means processing won't be called. - '' => 'No UI', - 'default' => 'Default UI', - 'admin' => 'Admin UI', - ]) + new OptionDefinition('', 'No UI'), + new OptionDefinition('default', 'Default UI', 'Uses the site theme for viewing and editing entities.'), + new OptionDefinition('admin', 'Admin UI', 'Uses the admin theme for viewing and editing entities.'), + ) ->setProcessing(function(DataItem $component_data) { $entity_data = $component_data->getParent(); if ($entity_data->handler_route_provider->isEmpty() || @@ -138,28 +138,39 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio // builder handler. $handler_property = PropertyDefinition::create('string') ->setLabel(ucfirst("{$handler_type_info['label']} handler")) - ->setOptionsArray([ - 'none' => 'Do not use a handler', - 'core' => 'Use the core handler class', - 'custom' => 'Provide a custom handler class', - ]); + ->setRequired(TRUE) + ->setOptions( + // We use an explicit empty option and make this required, so we + // can control the label used for this in the UI. + // Set weights and leave gaps so that static::getHandlerTypes() + // can insert options in between. + new OptionDefinition('none', 'Do not use a handler', weight: 0), + new OptionDefinition('core', 'Use the core handler class.', weight: 10), + new OptionDefinition('custom', 'Provide a custom handler class.', weight: 20), + ) + ->setLiteralDefault('none'); break; case 'custom_default': $default_handler_type = $handler_type_info['default_type']; $handler_property = PropertyDefinition::create('string') ->setLabel(ucfirst("{$handler_type_info['label']} handler")) - ->setOptionsArray([ - 'none' => 'Do not use a handler', - 'default' => "Use the '{$default_handler_type}' handler class (forces '{$default_handler_type}' to use the default if not set)", - 'custom' => "Provide a custom handler class (forces '{$default_handler_type}' to use the default if not set)", - ]) + ->setDescription("Setting a handler class here will force the '{$default_handler_type}' handler to be set to use the default handler class, if it is not set.'") + ->setRequired(TRUE) + ->setOptions( + // We use an explicit empty option and make this required, so we + // can control the label used for this in the UI. + new OptionDefinition('none', 'Do not use a handler', weight: 0), + new OptionDefinition('default', "Use the '{$default_handler_type}' handler class", weight: 10), + new OptionDefinition('custom', "Provide a custom handler class", weight: 20), + ) + ->setLiteralDefault('none') // Force the default type to at least be specified if it isn't // already. // TODO: this assumes the mode of the default handler type is // 'core_none'. ->setProcessing(function(DataItem $component_data) use ($default_handler_type) { - if ($component_data->isEmpty() || $component_data->value == 'none') { + if ($component_data->value == 'none') { // Nothing to do; this isn't set to use anything. return; } @@ -180,8 +191,8 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio // Add extra options specific to the handler type. if (isset($handler_type_info['options'])) { - foreach ($handler_type_info['options'] as $option_value => $option_label) { - $handler_property->addOption(new OptionDefinition($option_value, $option_label)); + foreach ($handler_type_info['options'] as $option) { + $handler_property->addOption($option); } } @@ -281,8 +292,9 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio * - 'custom_default': No handler is provided, but handler for another * type can be used. The option is whether to use that, or create a * custom handler. The 'default_type' property must also be given. - * - 'options': An array of additional options for the handler property. - * These are added to the options provided by the mode. + * - 'options': A array of additional OptionDefinition objects for the + * handler property. These are added to the options provided by the mode. + * Array keys are ignored. * - 'property_path': (optional) The path to set this into the annotation * beneath the 'handlers' key. Only required if this is not simply the * handler type key. @@ -309,8 +321,8 @@ protected static function getHandlerTypes() { 'options' => [ // Overwrite the label for the 'core' option which the mode provides. // This is OK because addOption() replaces an existing option. - 'core' => 'Default core route provider', - 'admin' => 'Admin route provider', + new OptionDefinition('core', 'Default core route provider', weight: 10), + new OptionDefinition('admin', 'Core admin route provider', weight: 15), ], 'options_classes' => [ 'default' => '\Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider', @@ -329,7 +341,7 @@ protected static function getHandlerTypes() { ], 'form_add' => [ 'label' => 'add form', - 'description' => "The entity form class for the 'add' operation.", + 'description' => "The entity form class for the 'add' operation. Setting a handler class here will force the 'default form' handler to be set to use the default handler class, if it is not set.", 'component_type' => 'EntityForm', 'property_path' => ['form', 'add'], 'class_name_suffix' => 'AddForm', @@ -339,7 +351,7 @@ protected static function getHandlerTypes() { ], 'form_edit' => [ 'label' => 'edit form', - 'description' => "The entity form class for the 'edit' operation.", + 'description' => "The entity form class for the 'edit' operation. Setting a handler class here will force the 'default form' handler to be set to use the default handler class, if it is not set.", 'component_type' => 'EntityForm', 'property_path' => ['form', 'edit'], 'class_name_suffix' => 'EditForm', @@ -371,6 +383,13 @@ protected static function getHandlerTypes() { ]; } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->entity_type_id->value ?: NULL; + } + /** * {@inheritdoc} */ @@ -462,7 +481,7 @@ public function requiredComponents(): array { 'prefix_name' => FALSE, 'plugin_name' => "entity.{$this->component_data['entity_type_id']}.add", 'plugin_properties' => [ - 'title' => 'Add ' . $this->component_data['entity_type_label'], + 'title' => 'Add ' . strtolower($this->component_data->entity_type_label->value), 'route_name' => "entity.{$this->component_data['entity_type_id']}.add_form", // Media module sets 10 for its tab; go further along. 'weight' => 15, diff --git a/Generator/EnvironmentAware.php b/Generator/EnvironmentAware.php new file mode 100644 index 00000000..84b0e31b --- /dev/null +++ b/Generator/EnvironmentAware.php @@ -0,0 +1,26 @@ +environment = $environment; + } + +} diff --git a/Generator/ExtensionCodeFile.php b/Generator/ExtensionCodeFile.php index fda0e084..4bc6ab9c 100644 --- a/Generator/ExtensionCodeFile.php +++ b/Generator/ExtensionCodeFile.php @@ -4,11 +4,14 @@ use DrupalCodeBuilder\File\CodeFile; use DrupalCodeBuilder\File\DrupalExtension; +use DrupalCodeBuilder\Generator\Render\DocBlock; /** * Generator class for procedural code files. */ -class ExtensionCodeFile extends PHPFile { +class ExtensionCodeFile extends PHPFile implements EnvironmentAware { + + use EnvironmentAwareTrait; /** * Whether this file is merged with existing code. @@ -59,6 +62,25 @@ public function getFileInfo(): CodeFile { ); } + /** + * Return the file doxygen header and any custom header code. + */ + function codeHeader() { + $docblock = DocBlock::file(); + + $docblock[] = $this->fileDocblockSummary(); + + $code = $docblock->render(); + // Blank line after the file docblock. + $code[] = ''; + + // Coding standards need this to go AFTER the @file docblock. + $code[] = 'declare(strict_types=1);'; + $code[] = ''; + + return $code; + } + /** * Return the main body of the file code. * @@ -216,7 +238,7 @@ function phpCodeBody() { foreach ($existing_import_nodes as $import_node) { $existing_import = $import_node->uses[0]->name->toString(); - $imported_classes[] = $existing_import; + $imported_classes[$existing_import] = NULL; } } @@ -245,7 +267,7 @@ function fileDocblockSummary() { * {@inheritdoc} */ function codeFooter() { - $footer = \DrupalCodeBuilder\Factory::getEnvironment()->getSetting('footer', NULL); + $footer = $this->environment->getSetting('footer', NULL); return $footer; } diff --git a/Generator/Form.php b/Generator/Form.php index 3f91b95b..c18c933b 100644 --- a/Generator/Form.php +++ b/Generator/Form.php @@ -49,6 +49,9 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio // Put the rest of the parent definitions after ours. $definition->addProperties($properties); + // Move the injected services property lower down. + $definition->movePropertyAfter('injected_services', 'form_route'); + $definition->getProperty('use_static_factory_method') ->setLiteralDefault(TRUE); @@ -66,6 +69,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLiteralDefault('\Drupal\Core\Form\FormBase'); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->plain_class_name->value ?: NULL; + } + /** * {@inheritdoc} */ diff --git a/Generator/FormElement.php b/Generator/FormElement.php index d467f258..d0ff204f 100644 --- a/Generator/FormElement.php +++ b/Generator/FormElement.php @@ -4,6 +4,7 @@ use MutableTypedData\Definition\PropertyListInterface; use DrupalCodeBuilder\Definition\PropertyDefinition; +use MutableTypedData\Data\DataItem; use MutableTypedData\Definition\DefaultDefinition; use MutableTypedData\Definition\OptionsSortOrder; @@ -41,6 +42,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ]); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->form_key->value ?: NULL; + } + /** * {@inheritdoc} */ diff --git a/Generator/HookTheme.php b/Generator/HookBodyHookTheme.php similarity index 58% rename from Generator/HookTheme.php rename to Generator/HookBodyHookTheme.php index 993a1428..85d22aa5 100644 --- a/Generator/HookTheme.php +++ b/Generator/HookBodyHookTheme.php @@ -3,18 +3,22 @@ namespace DrupalCodeBuilder\Generator; /** - * Generator for hook_theme() implementation. + * Generator for hook_theme() implementation body lines. + * + * @see \DrupalCodeBuilder\Generator\HookImplementationBase */ -class HookTheme extends HookImplementationProcedural { +class HookBodyHookTheme extends PHPFunctionBodyLines { /** * {@inheritdoc} */ - protected function getFunctionBody(): array { - // If we have no children, i.e. no ThemeHook components, then hand over to - // the parent, which will output the default hook code. + public function getContents(): array { + // If we have no children, i.e. no ThemeHook components, then return a + // return of empty array. if ($this->containedComponents->isEmpty()) { - return parent::getFunctionBody(); + return [ + 'return [];', + ]; } $code = []; @@ -24,8 +28,6 @@ protected function getFunctionBody(): array { } $code[] = '];'; - $this->component_data->body_indented = FALSE; - return $code; } diff --git a/Generator/HookImplementationBase.php b/Generator/HookImplementationBase.php index 7b1fb5dc..debb79a5 100644 --- a/Generator/HookImplementationBase.php +++ b/Generator/HookImplementationBase.php @@ -2,63 +2,198 @@ namespace DrupalCodeBuilder\Generator; -use MutableTypedData\Definition\PropertyListInterface; +use CaseConverter\CaseString; use DrupalCodeBuilder\Definition\PropertyDefinition; +use MutableTypedData\Data\DataItem; +use MutableTypedData\Definition\PropertyListInterface; +use MutableTypedData\Definition\VariantDefinition; /** * Abstract base class for hook implementations. + * + * This is specialised with child classes for: + * - class method hooks + * - procedural hooks + * - specific hooks that collect contents which are only procedural, e.g. + * hook_menu() + * - hook_updateN() which needs to change the function name. + * + * Furthermore, hooks that collect contents and can be procedural or OO use + * a hook body class, e.g. hook_theme(). */ -abstract class HookImplementationBase extends PHPFunction { +abstract class HookImplementationBase extends PHPFunction implements ClassHandlerAware { + + use ClassHandlerAwareTrait; + + /** + * {@inheritdoc} + */ + protected static $dataType = 'mutable'; /** * {@inheritdoc} */ public static function addToGeneratorDefinition(PropertyListInterface $definition) { - parent::addToGeneratorDefinition($definition); - - $definition->addProperties([ - // The name of the file that this hook implementation should be placed - // into. - // For HookImplementationClassMethod this is unused, but simpler to have - // this here rather than have Hooks decide whether to set it or not. Plus - // we might use it at some point to decide which class to use. - 'code_file' => PropertyDefinition::create('string') + // Make a dummy property list to get parent properties. + $common_properties = PropertyDefinition::create('complex'); + parent::addToGeneratorDefinition($common_properties); + + $variants = [ + 'literal' => VariantDefinition::create() + ->setLabel('Literal'), + 'tokenized' => VariantDefinition::create() + ->setLabel('Tokenized'), + ]; + + $definition->setProperties([ + 'hook_name' => PropertyDefinition::create('string') + ->setLabel('Hook') + ->setRequired(TRUE) + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportHookData')), + ]) + ->setVariantMappingProvider(\DrupalCodeBuilder\Factory::getTask('ReportHookData')) + ->setVariants($variants); + + $variants['literal']->addProperties([ + // The short hook name. + 'short_hook_name' => PropertyDefinition::create('string') ->setInternal(TRUE) - ->setLiteralDefault('%module.module'), - // The long hook name. - 'hook_name' => PropertyDefinition::create('string'), - // The first docblock line from the hook's api.php definition. - 'description' => PropertyDefinition::create('string'), + ->setCallableDefault(fn ($component_data) => preg_replace('@^hook_@', '', $component_data->getParent()->hook_name->value)), ]); - $definition->getProperty('function_docblock_lines')->getDefault() - // Expression Language lets us define arrays, which is nice. - ->setExpression("['Implements ' ~ get('..:hook_name') ~ '().']"); - - // This appears to be necessary even though it's not used. WTF! - $definition->getProperty('function_name') - ->setCallableDefault(function ($component_data) { - $long_hook_name = $component_data->getParent()->hook_name->value; - $short_hook_name = preg_replace('@^hook_@', '', $long_hook_name); - $function_name = '%module_' . $short_hook_name; - return $function_name; - }); - - // Hook bodies are just sample code from the code documentation, so if - // there are contained components, these should override the sample code. - $definition->getProperty('body_overriden_by_contained') - ->setLiteralDefault(TRUE); - - // Hook implementations have no @return documentation. - $definition->getProperty('return')->getProperty('omit_return_tag') - ->setLiteralDefault(TRUE); + $variants['tokenized']->addProperties([ + 'hook_name_parameters' => PropertyDefinition::create('string') + ->setLabel('Hook name replacement parameters') + ->setDescription("Replacement values for the tokens in a hook name such as 'FORM_ID' or 'ENTITY_TYPE'. Enter the values to replace these in their order in the hook name.") + ->setMultiple(TRUE), + // The short hook name, with tokens replaced with any given parameters. + // We replace tokens only once, in this property. Hook function / method + // name then derive from this. For class method hooks, this is also used + // for the attribute. + 'short_hook_name' => PropertyDefinition::create('string') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + $short_hook_name = preg_replace('@^hook_@', '', $component_data->getParent()->hook_name->value); + + $hook_name_parameters = $component_data->getParent()->hook_name_parameters->values(); + + // Split on the token boundaries, leaving the underscore outside the + // token. For example, + // 'form_FORM_ID_alter' becomes: + // - form_ + // - FORM_ID + // - _alter + // and 'ENTITY_TYPE_view' becomes + // - ENTITY_TYPE + // - _view + $pieces = preg_split( + '@( + # Token at the start of the hook name. + \b (?=[[:upper:]_]) + | + # Left edge of a token. + (?<=[[:lower:]]_) (?=[[:upper:]_]) + | + # Right edge of a token. + (?<=[[:upper:]_]) (?=_[[:lower:]]) + )@x', + $short_hook_name, + ); + + // Replace the tokens. + foreach ($pieces as $i => &$piece) { + // Replace each upper-cased piece with a parameter if there are + // enough parameters. + if (preg_match('@^[[:upper:]_]+$@', $piece)) { + if ($hook_name_parameters) { + // Use up each parameter, so the array empties out. + $parameter = array_shift($hook_name_parameters); + $piece = $parameter; + } + } + } + + $short_hook_name = implode('', $pieces); + return $short_hook_name; + }) + ]); + + foreach ($variants as $variant) { + $variant->addProperties($common_properties->getProperties()); + + $variant->addProperties([ + // The name of the file that this hook implementation should be placed + // into. + // For HookImplementationClassMethod this is unused, but simpler to have + // this here rather than have Hooks decide whether to set it or not. Plus + // we might use it at some point to decide which class to use. + 'code_file' => PropertyDefinition::create('string') + ->setInternal(TRUE) + ->setLiteralDefault('%module.module'), + // The long hook name. + 'hook_name' => PropertyDefinition::create('string') + ->setLabel('Hook') + ->setRequired(TRUE) + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportHookData')), + 'hook_info' => PropertyDefinition::create('mapping') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + $task_handler_report = \DrupalCodeBuilder\Factory::getTask('ReportHookData'); + $hook_info = $task_handler_report->getHookDeclarations()[strtolower($component_data->getParent()->hook_name->value)]; + return $hook_info; + }), + // The first docblock line from the hook's api.php definition. + 'description' => PropertyDefinition::create('string') + ->setInternal(TRUE), + ]); + + $variant->getProperty('function_docblock_lines')->getDefault() + // Expression Language lets us define arrays, which is nice. + ->setExpression("['Implements ' ~ get('..:hook_name') ~ '().']"); + + // This appears to be necessary even though it's not used. WTF! + $variant->getProperty('function_name') + ->setCallableDefault(function ($component_data) { + $short_hook_name = $component_data->getParent()->short_hook_name->value; + $function_name = '%module_' . $short_hook_name; + return $function_name; + }); + + // Hook bodies are just sample code from the code documentation, so if + // there are contained components, these should override the sample code. + $variant->getProperty('body_overriden_by_contained') + ->setLiteralDefault(TRUE); + + $variant->getProperty('body_indented') + ->setLiteralDefault(TRUE); + + // Hook implementations have no @return documentation. + $variant->getProperty('return')->getProperty('omit_return_tag') + ->setLiteralDefault(TRUE); + } } /** * {@inheritdoc} */ - public function getMergeTag() { - return $this->component_data['hook_name']; + public function requiredComponents(): array { + $components = parent::requiredComponents(); + + // Determine if there is a hook body generator for this hook. + // We need dynamic hook bodies to be a separate generator so they are + // orthogonal to hook implementations being prodecural/class methods. + $long_hook_name = $this->component_data->hook_name->value; + $hook_class_name = 'HookBody' . CaseString::snake($long_hook_name)->pascal(); + // Make the fully qualified class name. + $hook_class = $this->classHandler->getGeneratorClass($hook_class_name); + if (class_exists($hook_class)) { + $components['body'] = [ + 'component_type' => $hook_class_name, + 'containing_component' => '%requester', + ]; + } + + return $components; } /** diff --git a/Generator/HookImplementationClassMethod.php b/Generator/HookImplementationClassMethod.php index dc3743f8..c4a722c4 100644 --- a/Generator/HookImplementationClassMethod.php +++ b/Generator/HookImplementationClassMethod.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Generator; -use MutableTypedData\Definition\PropertyListInterface; +use CaseConverter\CaseString; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Generator\Render\PhpAttributes; +use MutableTypedData\Data\DataItem; +use MutableTypedData\Definition\PropertyListInterface; /** * Generator for a single OO hook implementation. - * - * This should not be requested directly; use the Hooks component instead. */ class HookImplementationClassMethod extends HookImplementationBase { @@ -19,39 +19,168 @@ class HookImplementationClassMethod extends HookImplementationBase { public static function addToGeneratorDefinition(PropertyListInterface $definition) { parent::addToGeneratorDefinition($definition); - $definition->addProperties([ - 'hook_method_name' => PropertyDefinition::create('string') - ->setInternal(TRUE), - ]); + // Change the options provider to exclude obligate procedural hooks. + $definition->getProperty('hook_name') + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportHookClassMethodData')); + + $variants = $definition->getVariants(); + + // For a literal hook, the method name is the camel case version of the + // short hook name, e.g. 'formAlter()'. + $variants['literal']->addProperty(PropertyDefinition::create('string') + ->setName('hook_method_name') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + $short_hook_name = $component_data->getParent()->short_hook_name->value; + return CaseString::snake($short_hook_name)->camel(); + }), + ); + + $variants['tokenized']->addProperty(PropertyDefinition::create('string') + ->setName('hook_method_name') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + // The short_hook_name already has had tokens replaced. + $short_hook_name = $component_data->getParent()->short_hook_name->value; + return CaseString::snake($short_hook_name)->camel(); + }), + ); + + // Add or update properties common to both variants. + foreach ($variants as $variant) { + // The address to get the class component that holds this method. + $variant->addProperty(PropertyDefinition::create('string') + ->setName('class_component_address') + ->setInternal(TRUE) + ); + + // This needs both a default and processing, as in some cases this gets + // set by the requester. + // TODO: always derive this in default. + $variant->getProperty('declaration') + ->setCallableDefault(function ($component_data) { + $hook_info = $component_data->getParent()->hook_info->value; + + $declaration = $hook_info['definition']; + + // Run the default through processing, as that's not done + // automatically. + $processing = $component_data->getDefinition()->getProcessing(); + $component_data->set($declaration); + $processing($component_data); + + return $component_data->get(); + }) + ->setProcessing(function(DataItem $component_data) { + // Replace the hook name from the hook info's declaration with the + // method name. + $declaration = preg_replace( + '/(?<=function )hook_(\w+)/', + $component_data->getParent()->hook_method_name->get(), + $component_data->get() + ); + + // Add the 'public' modifier. + $declaration = 'public ' . $declaration; + + $component_data->set($declaration); + }); + + $variant->getProperty('body') + ->setCallableDefault(function ($component_data) { + $hook_name = $component_data->getParent()->hook_name->value; + $hook_info = $component_data->getParent()->hook_info->value; + + $template = $hook_info['body']; + + // This needs to be split into an array of lines for things such as + // PHPFile::extractFullyQualifiedClasses() to work. + $template = explode("\n", $template); + + // Trim lines from start and end of body, as hook definitions + // have newlines at start and end. + $template = array_slice($template, 1, -1); + + return $template; + }); + + $variant->addProperties([ + 'class_name' => PropertyDefinition::create('string') + ->setInternal(TRUE), + ]); + } } /** * {@inheritdoc} */ - public function getContents(): array { - // Replace the hook name from the hook info's declaration with the method - // name. - $this->component_data->declaration->value = preg_replace( - '/(?<=function )hook_(\w+)/', - $this->component_data->hook_method_name->value, - $this->component_data->declaration->value - ); + public function getMergeTag() { + // Allow multiple copies of the same hook. + return NULL; + } + + /** + * {@inheritdoc} + */ + public function requiredComponents(): array { + $components = parent::requiredComponents(); - // Add the 'public' prefix. - $this->component_data->declaration->value = 'public ' . $this->component_data->declaration->value; + // Add a legacy procedural hook if required. + // Declaring the hooks class as a service for legacy is handled in + // HooksClass. + if ($this->component_data->getItem('module:hook_implementation_type')->value == 'oo_legacy') { + $hook_name = $this->component_data->hook_name->value; - return parent::getContents(); + $mb_task_handler_report = \DrupalCodeBuilder\Factory::getTask('ReportHookData'); + $hook_info = $mb_task_handler_report->getHookDeclarations()[strtolower($this->component_data->hook_name->value)]; + + $component_name = $hook_name . '_legacy'; + + $class_name_component_address = $this->component_data->class_component_address->value; + $hooks_class_name = $this->component_data->getItem($class_name_component_address)->qualified_class_name->value; + + // The legacy hook body is just a call to the hook method. + $hook_method_name = $this->component_data->hook_method_name->value; + // Get the parameters. + $matches = []; + preg_match_all('@(\$\w+)@', $hook_info['definition'], $matches); + $arguments = implode(', ', $matches[0]); + + // Use a return statement if the hook returns a value. + $return = !empty($hook_info['has_return']) ? 'return ' : ''; + + $legacy_method_body_line = "{$return}\Drupal::service(\\{$hooks_class_name}::class)->{$hook_method_name}({$arguments});"; + + $components[$component_name] = [ + // We don't need to check for specialised hook generators, as there's no + // special function body for the legacy hook. + 'component_type' => 'HookImplementationProcedural', + 'code_file' => $hook_info['destination'], + 'hook_name' => $hook_name, + 'short_hook_name' => $this->component_data->short_hook_name->value, + 'attribute' => 'Drupal\Core\Hook\LegacyHook', + 'description' => $hook_info['description'], + 'function_docblock_lines' => [ + 'Legacy hook implementation.', + '@todo Remove this method when support for Drupal core < 11.1 is dropped.', + ], + 'body' => [$legacy_method_body_line], + // The code is a single string, already indented. Ensure we don't + // indent it again. + 'body_indented' => FALSE, + ]; + } + + return $components; } /** * {@inheritdoc} */ protected function getFunctionAttributes(): array { - $short_hook_name = preg_replace('/^hook_/', '', $this->component_data->hook_name->value); - $attribute = PhpAttributes::method( '\Drupal\Core\Hook\Attribute\Hook', - $short_hook_name, + $this->component_data->short_hook_name->value, ); return [$attribute]; } diff --git a/Generator/HookImplementationProcedural.php b/Generator/HookImplementationProcedural.php index 292bfd46..f37cb099 100644 --- a/Generator/HookImplementationProcedural.php +++ b/Generator/HookImplementationProcedural.php @@ -2,42 +2,89 @@ namespace DrupalCodeBuilder\Generator; +use DrupalCodeBuilder\Definition\PropertyDefinition; +use MutableTypedData\Data\DataItem; +use MutableTypedData\Definition\PropertyListInterface; + /** * Generator for a single procedural function hook implementation. - * - * This should not be requested directly; use the Hooks component instead. */ class HookImplementationProcedural extends HookImplementationBase { + /** + * {@inheritdoc} + */ + public static function addToGeneratorDefinition(PropertyListInterface $definition) { + parent::addToGeneratorDefinition($definition); + + $variants = $definition->getVariants(); + + foreach ($variants as $variant) { + // The address to get the class component that holds this method. + $variant->addProperty(PropertyDefinition::create('string') + ->setName('class_component_address') + ->setInternal(TRUE) + ); + + $variant->getProperty('declaration') + ->setCallableDefault(function ($component_data) { + $hook_info = $component_data->getParent()->hook_info->value; + + $declaration = $hook_info['definition']; + + // Run the default through processing, as that's not done + // automatically. + $processing = $component_data->getDefinition()->getProcessing(); + $component_data->set($declaration); + $processing($component_data); + + return $component_data->get(); + }) + ->setProcessing(function(DataItem $component_data) { + $short_hook_name = $component_data->getParent()->short_hook_name->get(); + + // Replace the hook name from the hook info's declaration with the + // short hook name and the module prefix. + $declaration = preg_replace( + '/(?<=function )hook_(\w+)/', + '%module_' . $short_hook_name, + $component_data->get() + ); + + $component_data->set($declaration); + }); + } + } + /** * {@inheritdoc} */ public function requiredComponents(): array { + $components = parent::requiredComponents(); + $code_file = $this->component_data['code_file']; - return [ - 'code_file' => [ - 'component_type' => 'ExtensionCodeFile', - 'filename' => $code_file, - ], + $components['code_file'] = [ + 'component_type' => 'ExtensionCodeFile', + 'filename' => $code_file, ]; + + return $components; } /** * {@inheritdoc} */ - function containingComponent() { - return '%self:code_file'; + public function getMergeTag() { + // Use the short hook name, as that has tokens replaced. + return $this->component_data->short_hook_name->value; } /** * {@inheritdoc} */ - public function getContents(): array { - // Replace the 'hook_' part of the function declaration. - $this->component_data->declaration->value = preg_replace('/(?<=function )hook/', '%module', $this->component_data->declaration->value); - - return parent::getContents(); + function containingComponent() { + return '%self:code_file'; } } diff --git a/Generator/HookUpdateN.php b/Generator/HookUpdateN.php index e3750629..62b083d0 100644 --- a/Generator/HookUpdateN.php +++ b/Generator/HookUpdateN.php @@ -17,16 +17,20 @@ class HookUpdateN extends HookImplementationProcedural { public static function addToGeneratorDefinition(PropertyListInterface $definition) { parent::addToGeneratorDefinition($definition); - // The next schema number to use for the hook implementation. - $definition->addProperty(PropertyDefinition::create('string') - ->setName('schema_number') - ->setInternal(TRUE) - ->setCallableDefault(function ($component_data) { - // Get overwritten by detectExistence() if existing implementations are - // found. - return (\DrupalCodeBuilder\Factory::getEnvironment()->getCoreMajorVersion() * 1000) + 1; - }) - ); + // This hook is plain, not tokenised, so we don't need to add the property + // on both variants. + foreach ($definition->getVariants() as $variant) { + // The next schema number to use for the hook implementation. + $variant->addProperty(PropertyDefinition::create('string') + ->setName('schema_number') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + // Get overwritten by detectExistence() if existing implementations are + // found. + return (\DrupalCodeBuilder\Factory::getEnvironment()->getCoreMajorVersion() * 1000) + 1; + }) + ); + } } /** @@ -72,9 +76,9 @@ public function detectExistence(DrupalExtension $extension) { */ public function getContents(): array { // Replace the '_N' part of the function declaration. - $this->component_data->declaration->value = preg_replace('/(?<=hook_update_)N/', $this->component_data->schema_number->value, $this->component_data->declaration->value); + $this->component_data->declaration->value = preg_replace('/(?<=_update_)N/', $this->component_data->schema_number->value, $this->component_data->declaration->value); // Also do the function name. - $this->component_data->function_name->value = preg_replace('/(?<=update_)N/', $this->component_data->schema_number->value, $this->component_data->function_name->value); + $this->component_data->function_name->value = preg_replace('/(?<=_update_)N/', $this->component_data->schema_number->value, $this->component_data->function_name->value); return parent::getContents(); } diff --git a/Generator/Hooks.php b/Generator/Hooks.php index 13af5978..6e2b9e7d 100644 --- a/Generator/Hooks.php +++ b/Generator/Hooks.php @@ -26,20 +26,10 @@ * * @see DrupalCodeBuilder\Generator\ExtensionCodeFile */ -class Hooks extends BaseGenerator { +class Hooks extends BaseGenerator implements ClassHandlerAware, EnvironmentAware { - /** - * Theme hooks which remain procedural. - * - * TODO: Move this to analysis? Although there's no sodding documentation. - */ - const PROCEDURAL_HOOKS = [ - 'hook_theme', - 'hook_theme_suggestion_HOOK', - 'hook_preprocess_hook', - 'hook_process_hook', - 'hook_theme_suggestions_HOOK_alter', - ]; + use ClassHandlerAwareTrait; + use EnvironmentAwareTrait; /** * {@inheritdoc} @@ -112,8 +102,6 @@ public function requiredComponents(): array { // Add components for each hook. foreach ($file_hook_list as $hook_name => $hook) { - $hook['short_hook_name'] = preg_replace('@^hook_@', '', $hook_name); - // The body for the hook implementation can come either template code, // or the hook documentation's example code. (Note that this will be // overridden further down the line if the HookImplementation component @@ -122,7 +110,7 @@ public function requiredComponents(): array { // Strip out INFO: comments for advanced users. // This has to be done before we split this into lines. // TODO: No need to do this if this is hook api.php file sample code! - if (!\DrupalCodeBuilder\Factory::getEnvironment()->getSetting('detail_level', 0)) { + if (!$this->environment->getSetting('detail_level', 0)) { // Used to strip INFO messages out of generated file for advanced users. $pattern = '#\s+/\* INFO:(.*?)\*/#ms'; $hook['template'] = preg_replace($pattern, '', $hook['template']); @@ -157,23 +145,18 @@ protected function addHookComponents(array &$components, array $hook_info): void // If the hook implementation type is set to procedural, then it's // procedural. ($this->component_data->hook_implementation_type->value == 'procedural') - // Hooks marked as procedural in analysis data. + // Hooks marked as obligate procedural in analysis data. || !empty($hook_info['procedural']) // Hooks that go in the .install file are always procedural. - || ($hook_info['destination'] == '%module.install') - // Other random hooks that aren't documented as such are always procedural. - || in_array($hook_info['name'], static::PROCEDURAL_HOOKS); + || ($hook_info['destination'] == '%module.install'); if ($use_procedural_hook) { $this->addProceduralHookComponent($components, $hook_info); } else { + // The HooksClass and HookImplementationClassMethod generators take care + // of adding legacy components if needed. $this->addOoHookComponents($components, $hook_info); - - // If we want legacy procedural hooks too. - if ($this->component_data->hook_implementation_type->value == 'oo_legacy') { - $this->addLegacyProceduralHookComponent($components, $hook_info); - } } } @@ -194,7 +177,7 @@ protected function addOoHookComponents(array &$components, array $hook_info): vo // Make the hooks class. $components['hooks_class'] = [ - 'component_type' => 'PHPClassFile', + 'component_type' => 'HooksClass', 'plain_class_name' => $hooks_class_name, 'relative_namespace' => 'Hook', 'class_docblock_lines' => [ @@ -202,11 +185,6 @@ protected function addOoHookComponents(array &$components, array $hook_info): vo ], ]; - // Make the method name out of the short hook name in camel case. - // TODO this is crap with e.g. hook_form_FORM_ID_alter becomes - // formFORMIDAlter(). - $hook_method_name = CaseString::snake($hook_info['short_hook_name'])->camel(); - // Make the class method hook. This must have the same name as the component // added in self::addProceduralHookComponent(), so that other generators // that set this hook as their containing_component work in both cases. @@ -215,7 +193,6 @@ protected function addOoHookComponents(array &$components, array $hook_info): vo 'component_type' => 'HookImplementationClassMethod', 'code_file' => $hook_info['destination'], 'hook_name' => $hook_name, - 'hook_method_name' => $hook_method_name, 'declaration' => $hook_info['definition'], 'description' => $hook_info['description'], // Set the hook template as the method body. @@ -224,6 +201,7 @@ protected function addOoHookComponents(array &$components, array $hook_info): vo // indent it again. 'body_indented' => TRUE, 'containing_component' => '%requester:hooks_class', + 'class_component_address' => '..:..:requests:hooks_class', ]; } @@ -237,14 +215,12 @@ protected function addOoHookComponents(array &$components, array $hook_info): vo * @param array $hook_info * The array of hook info. */ - protected function addProceduralHookComponent(array &$components, array $hook_info, ?string $component_name = NULL): void { + protected function addProceduralHookComponent(array &$components, array $hook_info): void { $hook_name = $hook_info['name']; - $component_name ??= $hook_name; - $hook_class_name = $this->getHookImplementationComponentType($hook_info); // Add a procedural hook implementation. - $components[$component_name] = [ + $components[$hook_name] = [ 'component_type' => $hook_class_name, 'code_file' => $hook_info['destination'], 'hook_name' => $hook_name, @@ -258,72 +234,6 @@ protected function addProceduralHookComponent(array &$components, array $hook_in ]; } - /** - * Adds the components for a legacy procedural hook. - * - * Helper for addHookComponents(). - * - * @param array &$components - * The array of requested components, passed by reference. - * @param array $hook_info - * The array of hook info. - */ - protected function addLegacyProceduralHookComponent(array &$components, array $hook_info): void { - $hook_name = $hook_info['name']; - $component_name = $hook_name . '_legacy'; - $hooks_class_name = $this->component_data->getItem('module:root_name_pascal')->value . 'Hooks'; - - // Start with the procedural hook component. - $this->addProceduralHookComponent($components, $hook_info, $component_name); - - // Make the method name out of the short hook name in camel case. - // TODO this is crap with e.g. hook_form_FORM_ID_alter becomes - // formFORMIDAlter(). - $hook_method_name = CaseString::snake($hook_info['short_hook_name'])->camel(); - - $components[$component_name]['attribute'] = 'Drupal\Core\Hook\LegacyHook'; - - $components[$component_name]['function_docblock_lines'] = [ - 'Legacy hook implementation.', - '@todo Remove this method when support for Drupal core < 11.1 is dropped.', - ]; - - // Replace the hook body with a call to the Hooks class. - // Get the parameters. - $matches = []; - preg_match_all('@(\$\w+)@', $hook_info['definition'], $matches); - $arguments = implode(', ', $matches[0]); - - // Use a return statement if the hook returns a value. - $return = !empty($hook_info['has_return']) ? 'return ' : ''; - - $components[$component_name]['body'] = [ - "{$return}\Drupal::service(\Drupal\%extension\Hook\\{$hooks_class_name}::class)->{$hook_method_name}({$arguments});", - ]; - $components[$component_name]['body_indented'] = FALSE; - - // Explicitly declare the Hooks class as a service. - // ARGH, can't use the 'Service' generator, as that will want to create a - // class! - $yaml_data = [ - 'services' => [ - // Argh DRY class name! - // TODO: move the class name to being created in this generator. - 'Drupal\%extension\Hook\\' . $hooks_class_name => [ - 'class' => 'Drupal\%extension\Hook\\' . $hooks_class_name, - 'autowire' => TRUE, - ], - ], - ]; - $components['%module.services.yml'] = [ - 'component_type' => 'YMLFile', - // Probably have to use this deprecated token so the component merge - // works? - 'filename' => '%module.services.yml', - 'yaml_data' => $yaml_data, - ]; - } - /** * Gets the component type for the implementation of a hook. * @@ -491,8 +401,8 @@ function getTemplates($requested_hook_list) { // node.hooks.template will only override that same file in the module data; // if the hook is not requested as part of a group then that file will not be considered. // (Though groups are broken for now...) - $version = \DrupalCodeBuilder\Factory::getEnvironment()->getCoreMajorVersion(); - $template_base_path_module = \DrupalCodeBuilder\Factory::getEnvironment()->getPath('templates') . '/' . $version; + $version = $this->environment->getCoreMajorVersion(); + $template_base_path_module = $this->environment->getPath('templates') . '/' . $version; //print "base path: $template_base_path_module"; // $template_base_paths['module'] // $template_base_paths['user'] diff --git a/Generator/HooksClass.php b/Generator/HooksClass.php new file mode 100644 index 00000000..72664206 --- /dev/null +++ b/Generator/HooksClass.php @@ -0,0 +1,113 @@ +getProperty('service_tag_type')->setInternal(TRUE); + $definition->getProperty('service_name')->setInternal(TRUE); + $definition->getProperty('decorates')->setInternal(TRUE); + $definition->getProperty('tags')->setInternal(TRUE); + + // Move the form class name property to the top, and make it user-set rather + // than internal with a default. + $definition->getProperty('plain_class_name') + ->setLabel("Hooks class name") + ->setInternal(FALSE) + ->setDescription("The hooks class's plain class name, e.g. \"MyHooks\".") + ->setCallableDefault(function ($component_data) { + // Add a suffix to the default class name based on the human-readable + // index. + $delta = $component_data->getParent()->getName(); + $suffix = match ($delta) { + '0' => '', + default => $delta + 1, + }; + + return $component_data->getParent()->root_name_pascal->value . 'Hooks' . $suffix; + }); + $definition->getProperty('relative_namespace') + ->setDefault(DefaultDefinition::create() + ->setLiteral('Hook') + ); + + // The service name is the same as the class name. + $definition->getProperty('service_name_prefix')->setLiteralDefault(''); + $definition->getProperty('service_name')->setExpressionDefault("get('..:qualified_class_name')"); + $definition->getProperty('autowire')->setLiteralDefault(TRUE); + + $definition->getProperty('class_docblock_lines') + ->setDefault( + DefaultDefinition::create() + ->setLiteral(['Contains hook implementations for the %readable %base.']) + ); + + $definition->addPropertyBefore( + 'injected_services', + MergingGeneratorDefinition::createFromGeneratorType('HookImplementationClassMethod') + ->setName('hook_methods') + ->setDescription('Hook implementations in this class. The same hook can be added multiple times.') + ->setLabel('Hook implementations') + ->setMultiple(TRUE) + ->setProcessing(function(DataItem $component_data) { + $component_data->containing_component = '%requester'; + $component_data->class_component_address = '..:..'; + }), + ); + + $definition->addProperty(PropertyDefinition::create('string') + ->setName('root_name_pascal') + ->setInternal(TRUE) + ->setExpressionDefault("get('..:..:..:root_name_pascal')") + ); + } + + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->plain_class_name->value ?: NULL; + } + + /** + * {@inheritdoc} + */ + public static function findAdoptableComponents(DrupalExtension $extension): array { + // For now we don't adopt hook classes, so override this method so we don't + // return the same as the parent class. + return []; + } + + /** + * {@inheritdoc} + */ + public function requiredComponents(): array { + $components = parent::requiredComponents(); + + // If there's no legacy support, this service class doesn't need to be + // declared. + if ($this->component_data->getItem('module:hook_implementation_type')->value == 'oo') { + unset($components['%module.services.yml']); + } + + return $components; + } + +} + diff --git a/Generator/InjectedService.php b/Generator/InjectedService.php index 26d73ee2..b94c3341 100644 --- a/Generator/InjectedService.php +++ b/Generator/InjectedService.php @@ -144,25 +144,14 @@ public function requiredComponents(): array { if ($this->component_data->class_has_static_factory->value) { if ($service_info['type'] == 'service') { $container_extraction = "\$container->get('{$service_info['id']}'),"; - - $property_assignment = [ - 'id' => $service_info['id'], - 'property_name' => $service_info['property_name'], - 'variable_name' => $service_info['variable_name'], - ]; } else { // Pseudoservice: needs to be extracted from a real service. $container_extraction = "\$container->get('{$service_info['real_service']}')->{$service_info['service_method']}('{$service_info['variant']}'),"; - - $property_assignment = [ - 'id' => $service_info['id'], - 'property_name' => $service_info['property_name'], - 'variable_name' => $service_info['variable_name'], - 'parameter_extraction' => "{$service_info['real_service_variable_name']}->{$service_info['service_method']}('{$service_info['variant']}')", - ]; } + // Functions lines for the 'create' method get put inside the static + // create call: see PHPClassFileWithInjection. $components['create_line'] = [ 'component_type' => 'PHPFunctionBodyLines', 'containing_component' => '%requester:%requester:create', diff --git a/Generator/Library.php b/Generator/Library.php index 7fdf1ea8..6ee6dbe2 100644 --- a/Generator/Library.php +++ b/Generator/Library.php @@ -6,6 +6,7 @@ use MutableTypedData\Definition\DefaultDefinition; use DrupalCodeBuilder\Definition\MergingGeneratorDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; +use MutableTypedData\Data\DataItem; /** * Generator for a module library. @@ -55,6 +56,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ]); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->library_name->value ?: NULL; + } + /** * {@inheritdoc} */ diff --git a/Generator/Module.php b/Generator/Module.php index 51cc97d3..026965a0 100644 --- a/Generator/Module.php +++ b/Generator/Module.php @@ -222,6 +222,10 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'oo_legacy' => 'Both types, with legacy support for Drupal core < 11.1', ]) ->setLiteralDefault('oo_legacy'), + 'hook_classes' => MergingGeneratorDefinition::createFromGeneratorType('HooksClass') + ->setLabel('Hook classes') + ->setDescription('Classes that hold hook implementation methods. Will also generate legacy procedural functions if Hook implementation type is set to do so.') + ->setMultiple(TRUE), 'hooks' => PropertyDefinition::create('string') ->setLabel('Hook implementations') ->setMultiple(TRUE) @@ -275,7 +279,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'router_items' => MergingGeneratorDefinition::createFromGeneratorType('RouterItem') ->setLabel("Routes") ->setMultiple(TRUE), - 'dynamic_routes' => MergingGeneratorDefinition::createFromGeneratorType('RouteCallback') + 'dynamic_routes' => MergingGeneratorDefinition::createFromGeneratorType('DynamicRouteProvider') ->setLabel('Dynamic route providers') ->setMultiple(TRUE), 'library' => MergingGeneratorDefinition::createFromGeneratorType('Library') diff --git a/Generator/Module8.php b/Generator/Module8.php index cacaab45..3f5da043 100644 --- a/Generator/Module8.php +++ b/Generator/Module8.php @@ -26,6 +26,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLiteralDefault('procedural'); $definition->removeProperty('lifecycle'); + $definition->removeProperty('hook_classes'); } } diff --git a/Generator/PHPClassFile.php b/Generator/PHPClassFile.php index f0457fc0..f47e7392 100644 --- a/Generator/PHPClassFile.php +++ b/Generator/PHPClassFile.php @@ -235,6 +235,18 @@ public function getFileInfo(): CodeFile { ); } + /** + * {@inheritdoc} + */ + function fileHeader() { + return [ + " 'container', - 'typehint' => $this->containerInterface, + 'typehint' => static::CONTAINER_INTERFACE, ], ]; @@ -199,6 +208,30 @@ public function requiredComponents(): array { return $components; } + /** + * Produces the class declaration. + */ + function classDeclaration() { + if (static::CLASS_DI_INTERFACE && $this->needsDiInterface()) { + // Numeric key will clobber, so make something up! + // TODO: fix! + $this->component_data->interfaces->add(['ContainerInjectionInterface' => static::CLASS_DI_INTERFACE]); + } + + return parent::classDeclaration(); + } + + /** + * Determines whether the DI interface should be added. + * + * This is not called if static::CLASS_DI_INTERFACE is NULL. + * + * @return bool + */ + protected function needsDiInterface(): bool { + return !$this->component_data->injected_services->isEmpty(); + } + /** * The parameters for the base class. * diff --git a/Generator/PHPFile.php b/Generator/PHPFile.php index 8710b515..5777c33f 100644 --- a/Generator/PHPFile.php +++ b/Generator/PHPFile.php @@ -62,7 +62,9 @@ protected function fileContents() { /** * Return the PHP file header lines. */ - function fileHeader() { + function fileHeader() { + // Don't use strict_types here, as some PHP files such as API files don't + // use it, and ExtensionCodeFile needs special handling. return [ " $short_class) { + $clashes[$short_class][] = $full_class; + } + $clashes = array_filter($clashes, fn ($array) => count($array) > 1); + + $component_namespace = '\Drupal\\' . $this->component_data->root_component_name->value; + + foreach ($clashes as $short_class => $clash_set) { + // Rule 1: A non-Drupal class loses out to the Drupal class, and gets an + // alias with a prefix of its top-level namespace. + foreach ($clash_set as $full_class) { + if (!str_starts_with($full_class, '\Drupal')) { + $pieces = explode('\\', $full_class); + $alias = $pieces[1] . $short_class; + // ARGH! need to remake the marker-wrapped name! + $replacements['@IMPORT' . $full_class . 'IMPORT@'] = $alias; + + // Set the alias into the list of imported classes. + $imported_classes[$full_class] = $alias; + } + } + + // Rule 2: if multiple classes belong to the current module, prefix each + // one with the immediate parent namespace. + $clash_set_clashes_in_current_component = array_filter($clash_set, fn ($full_class) => str_starts_with($full_class, $component_namespace)); + if (count($clash_set_clashes_in_current_component) > 1) { + foreach ($clash_set_clashes_in_current_component as $full_class) { + $pieces = explode('\\', $full_class); + $alias = implode('', array_slice($pieces, -2)); + + $replacements['@IMPORT' . $full_class . 'IMPORT@'] = $alias; + $imported_classes[$full_class] = $alias; } } } - // Remove duplicates. - $imported_classes = array_unique($imported_classes); + // Replace the marker-wrapped full classes with the short classes in the + // whole code. + $class_code = str_replace(array_keys($replacements), array_values($replacements), $class_code); + + // Trim the initial '\' from the list of imported classes. + $new_keys = array_map(fn ($full_class) => ltrim($full_class, '\\'), array_keys($imported_classes)); + $imported_classes = array_combine($new_keys, array_values($imported_classes)); } /** * Produces the namespace import statements. * * @param $imported_classes - * (optional) An array of fully-qualified class names. The presence of the - * leading slash is immaterial. Duplicates are removed. + * An array of fully-qualified class names and aliases. Keys are fully-qualified class names, + * either with or without the leading slash. Values are one of: + * - NULL to indicate there is no alias. + * - The class name alias to use. */ - function imports($imported_classes = []) { - $imports = []; + function imports($imported_classes) { + $import_lines = []; if ($imported_classes) { - foreach ($imported_classes as $fully_qualified_class_name) { + foreach ($imported_classes as $fully_qualified_class_name => $alias) { $fully_qualified_class_name = ltrim($fully_qualified_class_name, '\\'); - $imports[] = "use $fully_qualified_class_name;"; + $import_lines[] = "use $fully_qualified_class_name" . ($alias ? " as $alias" : '') . ';'; } // Bit of a hack. We have to perform token replacement before sorting the @@ -175,7 +258,7 @@ function imports($imported_classes = []) { // replacement is done later, during file assembly. Fortunately, in // class names we can be certain that only the %extension and %Pascal // tokens are used, so hackily replace those now. - $imports = str_replace( + $import_lines = str_replace( [ '%extension', '%Pascal' @@ -184,19 +267,16 @@ function imports($imported_classes = []) { $this->component_data->root_component_name->value, CaseString::snake($this->component_data->root_component_name->value)->pascal(), ], - $imports, + $import_lines, ); // Sort the imported classes. - natcasesort($imports); - - // Remove duplicates. - $imports = array_unique($imports); + natcasesort($import_lines); - $imports[] = ''; + $import_lines[] = ''; } - return $imports; + return $import_lines; } /** diff --git a/Generator/PHPFunction.php b/Generator/PHPFunction.php index ced5a705..f6cfb071 100644 --- a/Generator/PHPFunction.php +++ b/Generator/PHPFunction.php @@ -406,7 +406,7 @@ protected function getFunctionAttributes(): array { * @return array * An array of code lines. */ - protected function buildMethodDeclaration($name, $parameters = [], $options = [], string $return_type = NULL): array { + protected function buildMethodDeclaration($name, $parameters = [], $options = [], ?string $return_type = NULL): array { $options += [ 'prefixes' => [], 'break_declaration' => FALSE, @@ -520,7 +520,8 @@ protected function getFunctionBody(): array { // There may be both property data and contained components. Contained // components override the body if it is set and if // 'body_overriden_by_contained' is TRUE. - $has_body_from_component_data = !$this->component_data->body->isEmpty(); + // Check values() rather than isEmpty() so defaults get applied. + $has_body_from_component_data = !empty($this->component_data->body->values()); $has_body_from_contained_components = $this->hasContainedComponentsOfContentType('line'); $let_body_from_contained_components_override_body_from_component_data = diff --git a/Generator/PHPUnitTest.php b/Generator/PHPUnitTest.php index 97046845..f8b0cf51 100644 --- a/Generator/PHPUnitTest.php +++ b/Generator/PHPUnitTest.php @@ -99,7 +99,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ], ], 'javascript' => [ - 'label' => 'Javascript test', + 'label' => 'JavaScript test', 'data' => [ 'force' => [ 'relative_namespace' => [ @@ -235,6 +235,18 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio $definition->getProperty('docblock_first_line')->setLiteralDefault("Test case class TODO."); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + $label = []; + $label[] = $data->test_type->value; + $label[] = $data->plain_class_name->value; + + return implode(' - ', array_filter($label)); + return $data->test_type->value ?: NULL; + } + /** * {@inheritdoc} */ @@ -368,7 +380,7 @@ protected function collectSectionBlocks() { 'modules', 'array', [ - 'docblock_first_line' => 'The modules to enable.', + 'docblock_inherit' => TRUE, 'prefixes' => ['protected', 'static'], 'default' => $test_install_modules, 'break_array_value' => TRUE, diff --git a/Generator/Permission.php b/Generator/Permission.php index 720d781b..fa22883a 100644 --- a/Generator/Permission.php +++ b/Generator/Permission.php @@ -5,6 +5,7 @@ use MutableTypedData\Definition\PropertyListInterface; use MutableTypedData\Definition\DefaultDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; +use MutableTypedData\Data\DataItem; /** * Generator for module permissions on Drupal 8 and higher. @@ -46,6 +47,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ]); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->permission->value ?: NULL; + } + /** * Return an array of subcomponent types. */ diff --git a/Generator/Plugin.php b/Generator/Plugin.php index e191e9ea..421f63db 100644 --- a/Generator/Plugin.php +++ b/Generator/Plugin.php @@ -5,6 +5,7 @@ use MutableTypedData\Definition\PropertyListInterface; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Definition\VariantGeneratorDefinition; +use MutableTypedData\Data\DataItem; use MutableTypedData\Definition\OptionsSortOrder; /** @@ -93,4 +94,21 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ]); } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + $label = []; + $label[] = $data->plugin_type->value; + + if ($data->hasProperty('plugin_label')) { + $label[] = $data->plugin_label->value; + } + else { + $label[] = $data->plugin_name->value; + } + + return implode(' - ', array_filter($label)); + } + } diff --git a/Generator/PluginClassBase.php b/Generator/PluginClassBase.php index 0d94bf7d..08d326ba 100644 --- a/Generator/PluginClassBase.php +++ b/Generator/PluginClassBase.php @@ -2,14 +2,23 @@ namespace DrupalCodeBuilder\Generator; +use MutableTypedData\Definition\PropertyListInterface; + /** * General class for plugin classes. * - * Used for plugin base classes, and as a base class for class-based discovery - * plugins. + * Used for: + * - plugin base classes + * - base class for class-based discovery plugins + * - custom plugin class for Yaml-based discovery plugins. */ class PluginClassBase extends PHPClassFileWithInjection { + /** + * {@inheritdoc} + */ + protected const CLASS_DI_INTERFACE = '\Drupal\Core\Plugin\ContainerFactoryPluginInterface'; + /** * The plugin type data. * @@ -43,6 +52,16 @@ class PluginClassBase extends PHPClassFileWithInjection { ] ]; + /** + * {@inheritdoc} + */ + public static function addToGeneratorDefinition(PropertyListInterface $definition) { + parent::addToGeneratorDefinition($definition); + + $definition->getProperty('use_static_factory_method') + ->setLiteralDefault(TRUE); + } + /** * {@inheritdoc} */ diff --git a/Generator/PluginClassDiscovery.php b/Generator/PluginClassDiscovery.php index c1455cfb..be663289 100644 --- a/Generator/PluginClassDiscovery.php +++ b/Generator/PluginClassDiscovery.php @@ -12,7 +12,9 @@ /** * Common base class for annotation and attribute plugins. */ -abstract class PluginClassDiscovery extends PluginClassBase { +abstract class PluginClassDiscovery extends PluginClassBase implements ClassHandlerAware { + + use ClassHandlerAwareTrait; function __construct($component_data) { // Set some default properties. @@ -42,9 +44,6 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio $definition->getProperty('relative_class_name')->setInternal(TRUE); - $definition->getProperty('use_static_factory_method') - ->setLiteralDefault(TRUE); - $definition->addPropertyBefore( 'plain_class_name', PropertyDefinition::create('string') @@ -92,15 +91,28 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'deriver' => PropertyDefinition::create('boolean') ->setLabel('Use deriver') ->setDescription("Adds a deriver class to dynamically derive plugins from a template."), + 'deriver_injected_services' => PropertyDefinition::create('string') + ->setLabel('Deriver injected services') + ->setDescription("Services to inject into the deriver class.") + ->setMultiple(TRUE) + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportServiceData')) + ->setDependencyValue([ + '..:deriver' => TRUE, + ]), 'deriver_plain_class_name' => PropertyDefinition::create('string') ->setInternal(TRUE) ->setDefault(DefaultDefinition::create() ->setCallable(function (DataItem $component_data) { $plugin_data = $component_data->getParent(); + $plugin_type_id = $plugin_data->plugin_type_data->value['type_id']; + + // Plugin type ID can contain dots from the service name. + $plugin_type_id = str_replace('.', '_', $plugin_type_id); + return $plugin_data->plain_class_name->value . - CaseString::snake($plugin_data->plugin_type_data->value['type_id'])->pascal() . + CaseString::snake($plugin_type_id)->pascal() . 'Deriver'; }) ), @@ -117,7 +129,10 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ), 'replace_parent_plugin' => PropertyDefinition::create('boolean') ->setLabel('Replace parent plugin') - ->setDescription("Replace the parent plugin's class with the generated class, rather than define a new plugin."), + ->setDescription("Replace the parent plugin's class with the generated class, rather than define a new plugin.") + ->setDependencyValue([ + '..:parent_plugin_id' => TRUE, + ]), 'class_docblock_lines' => PropertyDefinition::create('mapping') ->setInternal(TRUE) ->setDefault( @@ -232,16 +247,14 @@ public function requiredComponents(): array { if (!empty($this->component_data->deriver->value)) { $components['deriver'] = [ - 'component_type' => 'PHPClassFile', + 'component_type' => 'PluginDeriver', 'class_docblock_lines' => [ 'Plugin deriver for ' . $this->component_data->plugin_name->value . '.', ], 'plain_class_name' => $this->component_data->deriver_plain_class_name->value, 'relative_namespace' => 'Plugin\Derivative', 'parent_class_name' => '\Drupal\Component\Plugin\Derivative\DeriverBase', - 'interfaces' => [ - '\Drupal\Core\Plugin\Discovery\ContainerDeriverInterface', - ], + 'injected_services' => $this->component_data->deriver_injected_services->values(), ]; $components['getDerivativeDefinitions'] = [ @@ -299,7 +312,13 @@ function classDeclaration() { $this->component_data->parent_class_name->value = '\\' . $this->plugin_type_data['base_class']; } - // Set the DI interface if needed. + return parent::classDeclaration(); + } + + /** + * {@inheritdoc} + */ + protected function needsDiInterface(): bool { $use_di_interface = FALSE; // We need the DI interface if this class injects services, unless a parent // class also does so. @@ -323,13 +342,7 @@ function classDeclaration() { } } - if ($use_di_interface) { - // Numeric key will clobber, so make something up! - // TODO: fix! - $this->component_data->interfaces->add(['ContainerFactoryPluginInterface' => '\Drupal\Core\Plugin\ContainerFactoryPluginInterface']); - } - - return parent::classDeclaration(); + return $use_di_interface; } /** diff --git a/Generator/PluginDeriver.php b/Generator/PluginDeriver.php new file mode 100644 index 00000000..1d38bc8d --- /dev/null +++ b/Generator/PluginDeriver.php @@ -0,0 +1,77 @@ + 'base_plugin_id', + 'description' => 'The base plugin ID.', + 'typehint' => 'string', + ], + ]; + + /** + * {@inheritdoc} + */ + public static function addToGeneratorDefinition(PropertyListInterface $definition) { + parent::addToGeneratorDefinition($definition); + + $definition->getProperty('use_static_factory_method') + ->setLiteralDefault(TRUE); + } + + /** + * Produces the class declaration. + */ + function classDeclaration() { + if (!$this->needsDiInterface()) { + // Numeric key will clobber, so make something up! + // TODO: fix! + $this->component_data->interfaces->add(['CLASS_NO_DI_INTERFACE' => static::CLASS_NO_DI_INTERFACE]); + } + + return parent::classDeclaration(); + } + + /** + * {@inheritdoc} + */ + protected function getConstructBaseParameters() { + // Deriver classes do not pass on the $base_plugin_id create() parameter to + // the constructor. + return []; + } + + /** + * {@inheritdoc} + */ + protected function getCreateParameters() { + return static::STANDARD_FIXED_PARAMS; + } + +} diff --git a/Generator/PluginType.php b/Generator/PluginType.php index af873e2f..c8fb1cf3 100644 --- a/Generator/PluginType.php +++ b/Generator/PluginType.php @@ -5,15 +5,17 @@ use MutableTypedData\Definition\PropertyListInterface; use CaseConverter\CaseString; use MutableTypedData\Definition\DefaultDefinition; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Definition\MergingGeneratorDefinition; +use DrupalCodeBuilder\File\DrupalExtension; +use MutableTypedData\Data\DataItem; use MutableTypedData\Definition\VariantDefinition; /** * Generator for a plugin type. */ -class PluginType extends BaseGenerator { +class PluginType extends BaseGenerator implements AdoptableInterface { use NameFormattingTrait; @@ -30,11 +32,6 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLabel('Plugin discovery type') ->setDescription("The way in which plugins of this type are formed.") ->setOptions( - OptionDefinition::create( - 'annotation', - 'Annotation plugin', - "Each plugin is a class with an annotation to declare the plugin data. WARNING: This plugin discovery type will soon be deprecated in Drupal core." - ), OptionDefinition::create( 'attribute', 'Attribute plugin', @@ -44,7 +41,12 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'yaml', 'YAML plugin', "Plugins are declared in a single YAML file, and usually share the same class." - ) + ), + OptionDefinition::create( + 'annotation', + 'Annotation plugin', + "Each plugin is a class with an annotation to declare the plugin data. WARNING: This plugin discovery type will soon be deprecated in Drupal core." + ), ) ]) ->setVariants([ @@ -91,6 +93,24 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setDependencies('..:plugin_type') ) ->setValidators('machine_name'), + // Stupidly named so they switch between both class-based discovery + // variants. + 'attribute_properties' => PropertyDefinition::create('complex') + ->setLabel("Annotation properties") + ->setDescription("Properties for the plugin type's annotation class. These do not produce generated code, but are here to facilitate the conversion to attribute plugin types.") + ->setMultiple(TRUE) + ->setProperties([ + 'name' => PropertyDefinition::create('string') + ->setLabel('Parameter name') + ->setRequired(TRUE), + 'type' => PropertyDefinition::create('string') + ->setLabel('Parameter type') + ->setRequired(TRUE) + ->setLiteralDefault('string'), + 'description' => PropertyDefinition::create('string') + ->setLabel('Parameter description') + ->setLiteralDefault('TODO: parameter description.'), + ]), ]), 'attribute' => VariantDefinition::create() ->setLabel('Attribute plugin') @@ -273,6 +293,140 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio } } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->plugin_label->value ?: NULL; + } + + /** + * {@inheritdoc} + */ + public static function findAdoptableComponents(DrupalExtension $extension): array { + $services_filename = $extension->name . '.services.yml'; + if (!$extension->hasFile($services_filename)) { + return []; + } + + $yaml = $extension->getFileYaml($services_filename); + $service_names = array_keys($yaml['services']); + $plugin_manager_names = array_filter($service_names, fn ($name) => str_starts_with($name, 'plugin.manager.')); + $plugin_manager_names = array_map(fn ($name) => str_replace('plugin.manager.', '', $name), $plugin_manager_names); + return array_combine($plugin_manager_names, $plugin_manager_names); + } + + /** + * {@inheritdoc} + */ + public static function adoptComponent(DataItem $component_data, DrupalExtension $extension, string $property_name, string $name): void { + $services_filename = $extension->name . '.services.yml'; + $yaml = $extension->getFileYaml($services_filename); + $service_yaml = $yaml['services']['plugin.manager.' . $name]; + $manager_class = $service_yaml['class']; + + // The module might not be enabled, so we can't rely on Drupal's autoloader + // to find the class. + $extension->loadClass($manager_class); + + try { + $constructor_reflection = new \DrupalCodeBuilder\Utility\CodeAnalysis\Method($manager_class, '__construct'); + } + catch (\ReflectionException $e) { + // TODO: complain. + return; + } + + // Create the data for the plugin type. + $plugin_type_data_array = [ + 'plugin_type' => $name, + ]; + + $constructor_body = $constructor_reflection->getBody(); + + // Check for Attribute first, because of BC-compatible hybrids. + if (str_contains($constructor_body, "Drupal\\{$extension->name}\\Attribute")) { + // TODO + $plugin_type_data_array['discovery_type'] = 'attribute'; + + $matches = []; + preg_match("@Drupal\\\\{$extension->name}\\\\Attribute\\\\(\w+)@", $constructor_body, $matches); + } + elseif (str_contains($constructor_body, "Drupal\\{$extension->name}\\Annotation")) { + $plugin_type_data_array['discovery_type'] = 'annotation'; + + $matches = []; + preg_match("@Plugin/([\w/]+)@", $constructor_body, $matches); + $plugin_type_data_array['plugin_subdirectory'] = $matches['1'] ?? ''; + + $matches = []; + preg_match("@Drupal\\\\{$extension->name}\\\\Annotation\\\\([[\w\\]]+)@", $constructor_body, $matches); + $annotation_class = $matches[0]; + $plugin_type_data_array['annotation_class'] = $matches['1'] ?? ''; + + $extension->loadClass($annotation_class); + $annotation_class_reflection = new \ReflectionClass($annotation_class); + + // Get the properties from the annotation. + $annotation_property_relections = $annotation_class_reflection->getProperties(); + foreach ($annotation_property_relections as $property_reflection) { + $name = $property_reflection->getName(); + + // Skip the properties which are added automatically. + if (in_array($name, ['id', 'label', 'description'])) { + continue; + } + + // Skip the 'definition' property from the parent class. + if ($name == 'definition') { + continue; + } + + $property_data = [ + 'name' => $name, + ]; + + // Get the type of the property, defaulting to 'string' if we can't. + if ($property_reflection->hasType()) { + $property_data['type'] = (string) $property_reflection->getType(); + } + elseif ($docblock = $property_reflection->getDocComment()) { + $matches = []; + preg_match('/@var (\S+)$/m', $docblock, $matches); + if (!isset($matches[1])) { + $property_data['type'] = 'string'; + continue; + } + + $type = $matches[1]; + + if (str_ends_with($type, '[]')) { + $type = 'array'; + } + + $property_data['type'] = $type; + } + else { + // No sodding docs! + $property_data['type'] = 'string'; + } + $plugin_type_data_array['attribute_properties'][] = $property_data; + } + } + else { + // TODO! + $plugin_type_data_array['discovery_type'] = 'yaml'; + } + + // TODO: Find an existing component and merge into it -- see Service + // generator for example. + + // Bit of a WTF: this requires this generator class to know it's being used + // as a multi-valued item in the Module generator. + $item_data = $component_data->getItem($property_name)->createItem(); + $item_data->set($plugin_type_data_array); + } + /** * {@inheritdoc} */ @@ -302,6 +456,11 @@ public function requiredComponents(): array { // TODO: a service should be able to detect the parent class name from // service definitions.... if we had all of them. $components['manager']['parent_class_name'] = '\Drupal\Core\Plugin\DefaultPluginManager'; + + $components['manager']['metadata_class'] = match ($this->component_data->discovery_type->value) { + 'attribute' => $this->component_data->attribute_class->value, + 'annotation' => $this->component_data->annotation_class->value, + }; } else { // YAML plugin managers need some services injecting. @@ -420,19 +579,12 @@ public function requiredComponents(): array { ] ]; - $base_class_interfaces = [ - $this->component_data->interface->value, - ]; - if (!$this->component_data->base_class_injected_services->isEmpty()) { - $base_class_interfaces[] = '\Drupal\Core\Plugin\ContainerFactoryPluginInterface'; - } - $components['base_class'] = [ 'component_type' => 'PluginClassBase', 'plain_class_name' => $this->component_data['base_class_short_name'], 'relative_namespace' => 'Plugin\\' . $this->component_data['plugin_relative_namespace'], 'parent_class_name' => '\Drupal\Component\Plugin\PluginBase', - 'interfaces' => $base_class_interfaces, + 'interfaces' => [$this->component_data->interface->value], 'injected_services' => $this->component_data->base_class_injected_services->values(), 'use_static_factory_method' => TRUE, // Abstract for annotation or attribute plugins, where each plugin diff --git a/Generator/PluginTypeManager.php b/Generator/PluginTypeManager.php index a4be268c..2d833400 100644 --- a/Generator/PluginTypeManager.php +++ b/Generator/PluginTypeManager.php @@ -41,6 +41,13 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setAutoAcquiredFromRequester() ); } + + // The short class name of the attribute or annotation. + // Can't acquire this, as the property in the requester is on the variant. + $definition->addProperty( + PropertyDefinition::create('string') + ->setName('metadata_class') + ); } /** @@ -108,7 +115,7 @@ public function requiredComponents(): array { $components['service_cache.discovery']['omit_assignment'] = TRUE; } - // Only annotation type plugins call the parent constructor. + // Only class-based discovery plugins call the parent constructor. $code = []; if ($this->component_data->discovery_type->value == 'annotation') { $code[] = 'parent::__construct('; @@ -120,9 +127,7 @@ public function requiredComponents(): array { 'Drupal', $this->component_data['root_component_name'], 'Annotation', - // We can't acquire the annotation class name, as it's a mutable - // property and so not always present. Use this instead. - $this->component_data['plugin_plain_class_name'], + $this->component_data['metadata_class'], ]) . '::class'; $code[] = ');'; $code[] = ''; @@ -137,12 +142,20 @@ public function requiredComponents(): array { 'Drupal', $this->component_data['root_component_name'], 'Attribute', - // We can't acquire the attribute class name, as it's a mutable - // property and so not always present. Use this instead. - $this->component_data['plugin_plain_class_name'], - ]) . '::class'; - // Don't bother setting an annotation for BC, since we're generating a new - // plugin type. + $this->component_data['metadata_class'], + ]) . '::class,'; + $annotation_class_name = $this->makeQualifiedClassName([ + 'Drupal', + $this->component_data['root_component_name'], + 'Annotation', + $this->component_data['metadata_class'], + ]); + // Add BC support for annotations if an annotation class exists. This may + // happen when re-generating or adopting an existing plugin type. + if (class_exists($annotation_class_name)) { + $code[] = ' // @todo: Remove this parameter if not supporting BC annotation plugins.'; + $code[] = " " . '\\' . $annotation_class_name . '::class,'; + } $code[] = ');'; $code[] = ''; } diff --git a/Generator/PluginValidationConstraint.php b/Generator/PluginValidationConstraint.php index 768e0b75..99c7d824 100644 --- a/Generator/PluginValidationConstraint.php +++ b/Generator/PluginValidationConstraint.php @@ -17,11 +17,13 @@ class PluginValidationConstraint extends PluginClassDiscoveryHybrid { * Return an array of subcomponent types. */ public function requiredComponents(): array { - $components = parent::requiredComponents(); + $components = []; $components['validator'] = [ - 'component_type' => 'PHPClassFile', + 'component_type' => 'PHPClassFileWithInjection', 'plain_class_name' => $this->component_data['plain_class_name'] . 'Validator', + 'injected_services' => $this->component_data->injected_services->values(), + 'use_static_factory_method' => TRUE, 'relative_namespace' => $this->component_data['relative_namespace'], 'parent_class_name' => '\Symfony\Component\Validator\ConstraintValidator', 'docblock_first_line' => "Validates the {$this->component_data['plain_class_name']} constraint.", @@ -30,6 +32,12 @@ public function requiredComponents(): array { // See https://github.com/drupal-code-builder/drupal-code-builder/issues/134 ]; + // Zap the injected services set here, as we don't want the plugin class to + // have any DI. + $this->component_data->injected_services = []; + + $components += parent::requiredComponents(); + $components['validator_validate'] = [ 'component_type' => 'PHPFunction', 'function_name' => 'validate', diff --git a/Generator/PluginYamlDiscovery.php b/Generator/PluginYamlDiscovery.php index f786dfdf..ea152412 100644 --- a/Generator/PluginYamlDiscovery.php +++ b/Generator/PluginYamlDiscovery.php @@ -33,6 +33,14 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'deriver' => PropertyDefinition::create('boolean') ->setLabel('Use deriver') ->setDescription("Adds a deriver class to dynamically derive plugins from a template."), + 'deriver_injected_services' => PropertyDefinition::create('string') + ->setLabel('Deriver injected services') + ->setDescription("Services to inject into the deriver class.") + ->setMultiple(TRUE) + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportServiceData')) + ->setDependencyValue([ + '..:deriver' => TRUE, + ]), 'deriver_plain_class_name' => PropertyDefinition::create('string') ->setInternal(TRUE) ->setDefault(DefaultDefinition::create() @@ -51,6 +59,50 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio 'Deriver'; }) ), + 'plugin_custom_class' => PropertyDefinition::create('boolean') + ->setLabel('Use a custom plugin class') + ->setDescription("Adds a custom class for the plugin which inherits from the default."), + 'plugin_custom_class_parent' => PropertyDefinition::create('string') + ->setInternal(TRUE) + ->setCallableDefault(function ($component_data) { + $plugin_class_parent = '\\' . ( + $component_data->getParent()->plugin_type_data->value['base_class'] + ?? + $component_data->getParent()->plugin_type_data->value['yaml_properties']['class'] + ); + return $plugin_class_parent; + }), + 'plugin_custom_relative_class_name' => PropertyDefinition::create('string') + ->setInternal(TRUE) + ->setDefault( + DefaultDefinition::create() + ->setCallable(function (DataItem $component_data) { + // We need to use a reasonable namespace beneath Plugin for the + // class. Deriving it from the base class is too complex, as the + // class could be in the top-level namespace, or in Plugin. + // Instead, taking the type ID and forming namespaces from its + // dot-separated pieces is a best guess. + $plugin_type_id = $component_data->getParent()->plugin_type_data->value['type_id']; + + $suffix_pieces = array_map( + fn ($piece) => CaseString::snake($piece)->pascal(), + explode('.', $plugin_type_id), + ); + + $plugin_subdir = implode('\\', $suffix_pieces); + + $component_data->value = 'Plugin\\' . $plugin_subdir . '\\' . CaseString::snake($component_data->getParent()->plugin_name->value)->pascal(); + }) + ->setDependencies('..:plugin_custom_class') + ), + 'injected_services' => PropertyDefinition::create('string') + ->setLabel('Injected services for custom class') + ->setDescription("Services to inject if using a custom plugin class.") + ->setMultiple(TRUE) + ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportServiceData')) + ->setDependencyValue([ + '..:plugin_custom_class' => TRUE, + ]), 'prefix_name' => PropertyDefinition::create('boolean') ->setInternal(TRUE) ->setLiteralDefault(TRUE), @@ -125,6 +177,12 @@ public static function defaultPluginProperties($data_item) { $plugin_properties_with_defaults[$property_name] = $property_default; } } + + // Set the class if we're generating a custom plugin class. + if (!empty($data_item->getParent()->plugin_custom_class->value)) { + $plugin_properties_with_defaults['class'] = '\\Drupal\%module\\' . $data_item->getParent()->plugin_custom_relative_class_name->value; + } + return $plugin_properties_without_defaults + $plugin_properties_with_defaults; } @@ -143,16 +201,14 @@ public function requiredComponents(): array { if (!empty($this->component_data->deriver->value)) { $components['deriver'] = [ - 'component_type' => 'PHPClassFile', + 'component_type' => 'PluginDeriver', 'class_docblock_lines' => [ 'Plugin deriver for ' . $this->component_data->plugin_name->value . '.', ], 'plain_class_name' => $this->component_data->deriver_plain_class_name->value, 'relative_namespace' => 'Plugin\Derivative', 'parent_class_name' => '\Drupal\Component\Plugin\Derivative\DeriverBase', - 'interfaces' => [ - '\Drupal\Core\Plugin\Discovery\ContainerDeriverInterface', - ], + 'injected_services' => $this->component_data->deriver_injected_services->values(), ]; $components['getDerivativeDefinitions'] = [ @@ -168,6 +224,25 @@ public function requiredComponents(): array { ]; } + if (!empty($this->component_data->plugin_custom_class->value)) { + $plugin_class_parent = '\\' . ( + $this->component_data['plugin_type_data']['base_class'] + ?? + $this->component_data['plugin_type_data']['yaml_properties']['class'] + ); + + $components['plugin_custom_class'] = [ + 'component_type' => 'PluginClassBase', + 'class_docblock_lines' => [ + 'Plugin class for ' . $this->component_data->plugin_name->value . '.', + ], + // Use relative class name so we only compute one value. + 'relative_class_name' => $this->component_data->plugin_custom_relative_class_name->value, + 'parent_class_name' => $this->component_data->plugin_custom_class_parent->value, + 'injected_services' => $this->component_data->injected_services->values(), + ]; + } + return $components; } diff --git a/Generator/Profile.php b/Generator/Profile.php index 456d5960..677ac191 100644 --- a/Generator/Profile.php +++ b/Generator/Profile.php @@ -6,7 +6,7 @@ use MutableTypedData\Definition\PropertyListInterface; use DrupalCodeBuilder\Definition\PropertyDefinition; use MutableTypedData\Definition\DefaultDefinition; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; /** * Component generator: profile. diff --git a/Generator/Render/ClassAnnotation.php b/Generator/Render/ClassAnnotation.php index 43b759a8..9fddd077 100644 --- a/Generator/Render/ClassAnnotation.php +++ b/Generator/Render/ClassAnnotation.php @@ -157,6 +157,15 @@ protected function renderArray(&$docblock_lines, array $data, $nesting = 0, $ann $declaration_line .= "{$value},"; $docblock_lines[] = $declaration_line; } + elseif (is_bool($value) || is_null($value)) { + $declaration_line .= match ($value) { + NULL => 'NULL', + FALSE => 'FALSE', + TRUE => 'TRUE', + }; + $declaration_line .= ','; + $docblock_lines[] = $declaration_line; + } elseif (is_object($value)) { // Child annotation. The nesting level doesn't increase here, as the // child annotation class is just on the same line as the key. diff --git a/Generator/Render/PhpAttributes.php b/Generator/Render/PhpAttributes.php index f628f6d3..c8a2217a 100644 --- a/Generator/Render/PhpAttributes.php +++ b/Generator/Render/PhpAttributes.php @@ -72,21 +72,24 @@ public function forceInline(): self { /** * Renders the attribute to an array of code lines. */ - public function render() { + public function render(): array { $lines = []; $class_name_prefix = str_starts_with($this->attributeClassName, '\\') ? '' : '\\'; $class_name = $class_name_prefix . $this->attributeClassName; if (empty($this->data)) { + // Just the attribute class. $lines[] = '#[' . $class_name . ']'; } elseif (is_scalar($this->data)) { + // Attribute with a single scalar value. $lines[] = '#[' . $class_name . '(' . PhpValue::create($this->data)->renderInline() . ')]'; } elseif ($this->forceInline) { + // Attribute with an inline array. $lines[] = '#[' . $class_name . '(' . @@ -94,6 +97,7 @@ public function render() { ')]'; } else { + // Attribute with multi-line data. $lines[] = '#[' . $class_name . '('; $this->renderAttributeParameters($lines, $this->data); diff --git a/Generator/Render/PhpRenderer.php b/Generator/Render/PhpRenderer.php index da81da14..85464705 100644 --- a/Generator/Render/PhpRenderer.php +++ b/Generator/Render/PhpRenderer.php @@ -10,6 +10,16 @@ abstract class PhpRenderer { /** * Renders a scalar or NULL value as a PHP string. * + * To render: + * - A class or an expression starting with a class such as a class constant + * or the special '::class' expression, pass the fully-qualified classname + * or class constant as a string starting with '\\'. + * - A variable, pass the variable name as a string starting with '£' instead + * of '$'. + * - A boolean, pass either a literal boolean or a string such as 'TRUE'. + * - NULL, either the NULL value or the string 'NULL'. + * - A numeric value, either the value or the quoted value. + * * @param mixed $value * The value to render. * diff --git a/Generator/RootComponent.php b/Generator/RootComponent.php index d19edd88..f003c474 100644 --- a/Generator/RootComponent.php +++ b/Generator/RootComponent.php @@ -10,7 +10,7 @@ use DrupalCodeBuilder\MutableTypedData\DrupalCodeBuilderDataItemFactory; use MutableTypedData\Data\DataItem; use MutableTypedData\Definition\DefaultDefinition; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; /** * Abstract Generator for root components. @@ -117,6 +117,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setInternal(TRUE), 'lifecycle' => PropertyDefinition::create('string') ->setLabel('Lifecycle') + ->setDescription("Describes the stability of a module. Modules with a lifecycle value set will show with a warning on the module installation form.") ->setOptions( OptionDefinition::create('experimental', 'Experimental', weight: 0), OptionDefinition::create('deprecated', 'Deprecated', weight: 10), diff --git a/Generator/RouterItem.php b/Generator/RouterItem.php index cc770f0c..08913b27 100644 --- a/Generator/RouterItem.php +++ b/Generator/RouterItem.php @@ -56,7 +56,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLabel('The title for the menu tab') ->setLiteralDefault('My Page'), 'base_route' => PropertyDefinition::create('string') - ->setLabel('Route that this tab shows on') + ->setLabel('Base route that this tab shows on') ]), // TODO: remove this if possible? Probably need to allow PHPClassFile @@ -407,6 +407,13 @@ public static function controllerRelativeClassFromRoutePath(string $path) { return $controller_class_name; } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->path->value ?: NULL; + } + /** * {@inheritdoc} */ diff --git a/Generator/Service.php b/Generator/Service.php index c72993b5..7c5b704e 100644 --- a/Generator/Service.php +++ b/Generator/Service.php @@ -16,6 +16,11 @@ */ class Service extends PHPClassFileWithInjection implements AdoptableInterface { + /** + * {@inheritdoc} + */ + protected const CLASS_DI_INTERFACE = NULL; + use NameFormattingTrait; /** @@ -23,8 +28,8 @@ class Service extends PHPClassFileWithInjection implements AdoptableInterface { */ public static function addToGeneratorDefinition(PropertyListInterface $definition) { // Create the presets definition for service tag type property. - $task_handler_report_services = \DrupalCodeBuilder\Factory::getTask('ReportServiceData'); - $service_types_data = $task_handler_report_services->listServiceTypeData(); + $task_handler_report_service_tags = \DrupalCodeBuilder\Factory::getTask('ReportServiceTags'); + $service_types_data = $task_handler_report_service_tags->listServiceTagData(); $presets = []; foreach ($service_types_data as $type_tag => $type_data) { // Form the suggested service name from the last portion of the tag, thus: @@ -35,6 +40,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio $presets[$type_tag] = [ // Option label. 'label' => $type_data['label'], + 'api_url' => $type_data['api_url'] ?? '', 'data' => [ // Values that are forced on other properties. // These are set in the process stage. @@ -120,6 +126,7 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ), 'decorates' => PropertyDefinition::create('string') ->setLabel('Decorated service') + ->setDescription("The name of a service that this service decorates.") ->setOptionSetDefinition(\DrupalCodeBuilder\Factory::getTask('ReportServiceData')), // The parent service name. 'parent' => PropertyDefinition::create('string') @@ -185,6 +192,13 @@ public static function defaultRelativeNamespace($data_item) { } } + /** + * {@inheritdoc} + */ + public static function getDifferentiatedLabelSuffix(DataItem $data): ?string { + return $data->service_name->value ?: NULL; + } + /** * {@inheritdoc} */ @@ -196,6 +210,20 @@ public static function findAdoptableComponents(DrupalExtension $extension): arra $yaml = $extension->getFileYaml($services_filename); $service_names = array_keys($yaml['services']); + + // Filter out services that are adopted by other components, or aren't yet + // adoptable. + $service_names = array_filter( + $service_names, + fn ($name) => !( + // Filter out plugin managers, as these are adopted as part of a plugin + // type. + str_starts_with($name, 'plugin.manager.') || + // Filter out hook classes. + str_contains($name, '\Hook\\') + ), + ); + return array_combine($service_names, $service_names); } @@ -213,12 +241,15 @@ public static function adoptComponent(DataItem $component_data, DrupalExtension $value = [ 'service_name' => preg_replace("@^{$extension->name}\.@", '', $name), - 'injected_services' => array_map(fn ($service_name) => ltrim($service_name, '@'), $service_yaml['arguments']), // These properties are hidden in the UI but will be stored anyway. 'plain_class_name' => end($class_name_pieces), 'relative_namespace' => implode('\\', array_slice($class_name_pieces, 2, -1)), ]; + if (!empty($service_yaml['arguments'])) { + $value['injected_services'] = array_map(fn ($service_name) => ltrim($service_name, '@'), $service_yaml['arguments']); + } + foreach ($component_data->getItem($property_name) as $delta => $delta_item) { if ($delta_item->service_name->value == $value['service_name']) { $merge_delta = $delta; diff --git a/Generator/ServiceEventSubscriber.php b/Generator/ServiceEventSubscriber.php index fb27daec..f3dc7b57 100644 --- a/Generator/ServiceEventSubscriber.php +++ b/Generator/ServiceEventSubscriber.php @@ -4,6 +4,7 @@ use CaseConverter\CaseString; use DrupalCodeBuilder\Definition\PropertyDefinition; +use DrupalCodeBuilder\File\DrupalExtension; use MutableTypedData\Definition\PropertyListInterface; use MutableTypedData\Definition\OptionsSortOrder; @@ -55,6 +56,15 @@ public static function addToGeneratorDefinition(PropertyListInterface $definitio ->setLiteralDefault('EventSubscriber'); } + /** + * {@inheritdoc} + */ + public static function findAdoptableComponents(DrupalExtension $extension): array { + // For now we don't adopt event subscribers, so override this method so we + // don't return the same as the parent class. + return []; + } + /** * {@inheritdoc} */ diff --git a/Generator/ThemeHook.php b/Generator/ThemeHook.php index 307756c5..55743b91 100644 --- a/Generator/ThemeHook.php +++ b/Generator/ThemeHook.php @@ -53,7 +53,7 @@ public function requiredComponents(): array { * {@inheritdoc} */ function containingComponent() { - return '%self:hooks:hook_theme'; + return '%self:hooks:hook_theme:body'; } /** diff --git a/MutableTypedData/Data/DeltaLabelTrait.php b/MutableTypedData/Data/DeltaLabelTrait.php new file mode 100644 index 00000000..584b519d --- /dev/null +++ b/MutableTypedData/Data/DeltaLabelTrait.php @@ -0,0 +1,35 @@ +getLabel(); + + // Just return the label if this isn't a delta, or if there's no associated + // generator. + if (!$this->isDelta() || empty($this->definition->generatorClass)) { + return $label; + } + + if ($qualifying_label = $this->definition->generatorClass::getDifferentiatedLabelSuffix($this)) { + $label .= ' - ' . $qualifying_label; + } + + return $label; + } + +} diff --git a/MutableTypedData/Data/MappingData.php b/MutableTypedData/Data/MappingData.php index 8b787036..2d044247 100644 --- a/MutableTypedData/Data/MappingData.php +++ b/MutableTypedData/Data/MappingData.php @@ -34,7 +34,7 @@ public function items(): array { /** * {@inheritdoc} */ - public function validate(): array { + public function validate(?bool $include_internal = FALSE): array { return []; } diff --git a/MutableTypedData/Data/MergeableComplexDataWithArrayAccess.php b/MutableTypedData/Data/MergeableComplexDataWithArrayAccess.php index 5b7f4447..5dc0fea5 100644 --- a/MutableTypedData/Data/MergeableComplexDataWithArrayAccess.php +++ b/MutableTypedData/Data/MergeableComplexDataWithArrayAccess.php @@ -8,6 +8,8 @@ class MergeableComplexDataWithArrayAccess extends ComplexData implements \ArrayA use DataItemArrayAccessTrait; + use DeltaLabelTrait; + use MergeableComplexDataTrait; } diff --git a/MutableTypedData/Data/MergeableMutableDataWithArrayAccess.php b/MutableTypedData/Data/MergeableMutableDataWithArrayAccess.php index f8e12de6..a3a01dc8 100644 --- a/MutableTypedData/Data/MergeableMutableDataWithArrayAccess.php +++ b/MutableTypedData/Data/MergeableMutableDataWithArrayAccess.php @@ -9,6 +9,8 @@ class MergeableMutableDataWithArrayAccess extends MutableData implements \ArrayA use DataItemArrayAccessTrait; + use DeltaLabelTrait; + use MergeableComplexDataTrait; /** diff --git a/Task/Analyse/TestTraits.php b/Task/Analyse/TestTraits.php index 2da824fd..0af1fa3a 100644 --- a/Task/Analyse/TestTraits.php +++ b/Task/Analyse/TestTraits.php @@ -2,12 +2,14 @@ namespace DrupalCodeBuilder\Task\Analyse; -use MutableTypedData\Definition\OptionSetDefininitionInterface; +use Composer\Autoload\ClassLoader; use DrupalCodeBuilder\Environment\EnvironmentInterface; use DrupalCodeBuilder\Task\Collect\CollectorBase; use DrupalCodeBuilder\Task\Report\SectionReportInterface; use DrupalCodeBuilder\Task\SectionReportSimpleCountTrait; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; +use Drupal\Core\Extension\ExtensionDiscovery; +use MutableTypedData\Definition\OptionSetDefininitionInterface; /** * Task helper for analysing and reporting on traits intended for use in tests. @@ -74,6 +76,36 @@ public function getJobList() { * {@inheritdoc} */ public function collect($job_list) { + // Set up an autoloader, as test code is not included in Composer's project + // autoloader. Traits may extend other traits, so we can't just include + // files naively. + $loader = new ClassLoader(); + + $drupal_test_dir = $this->environment->getRoot() . '/core/tests'; + + $loader->add('Drupal\\BuildTests', $drupal_test_dir); + $loader->add('Drupal\\Tests', $drupal_test_dir); + $loader->add('Drupal\\TestSite', $drupal_test_dir); + $loader->add('Drupal\\KernelTests', $drupal_test_dir); + $loader->add('Drupal\\FunctionalTests', $drupal_test_dir); + $loader->add('Drupal\\FunctionalJavascriptTests', $drupal_test_dir); + $loader->add('Drupal\\TestTools', $drupal_test_dir); + + $root = $this->environment->getRoot() . '/'; + + // We need to include all discovered modules, not just enabled ones, as our + // Finder looks at all code, and installed modules may have traits which + // inherit from non-installed modules. + // Safe to use this as ExtensionDiscovery was introduced in 8.0.0. + $discovery = new ExtensionDiscovery($root); + $discovery->setProfileDirectories([]); + $discovered_modules = $discovery->scan('module'); + foreach ($discovered_modules as $module_name => $module_extension) { + $loader->addPsr4('Drupal\\Tests\\' . $module_name . '\\', $root . $module_extension->getPath() . '/tests/src'); + } + + $loader->register(); + $finder = new \Symfony\Component\Finder\Finder(); $finder ->in($this->environment->getRoot()) @@ -120,10 +152,6 @@ public function collect($job_list) { $short_trait_name = $file->getFilenameWithoutExtension(); - // Files in test folders aren't in the regular Composer autoloader, so - // include the file so we can use reflection on the class. - include_once($relative_pathname); - $class_reflection = new \ReflectionClass($classname); $docblock = $class_reflection->getDocComment(); diff --git a/Task/Collect.php b/Task/Collect.php index 4dcdcb07..4bb34804 100644 --- a/Task/Collect.php +++ b/Task/Collect.php @@ -8,8 +8,10 @@ namespace DrupalCodeBuilder\Task; use DrupalCodeBuilder\Attribute\InjectImplementations; +use DrupalCodeBuilder\Environment\EnvironmentInterface; use DrupalCodeBuilder\Task\Collect\CollectorInterface; - +use DrupalCodeBuilder\Task\Collect\MetadataCollector; +use DrupalCodeBuilder\Utility\ArrayOrder; /** * Task handler for collecting and processing definitions for Drupal components. * @@ -51,6 +53,17 @@ class Collect extends Base { */ #[InjectImplementations(CollectorInterface::class)] public function setCollectors(array $collectors) { + // Append the metadata controller so it runs last. + // This is faffy, but the faff is small and contained. The alternatives + // would be: a weight system for interface-based injection + // (over-engineered), or injecting this collector separately (but then it + // can't implement the interface or use the base class, which requires more + // code). + // WARNING: We rely on the array of collectors being keyed by the service + // name, which is only the case because of a bug in PHP-DI which doesn't + // allow us to use the splat operator for the collectors parameter! + ArrayOrder::moveKeyToEnd($collectors, 'Collect\\MetadataCollector'); + $this->collectors = $collectors; } diff --git a/Task/Collect/CodeAnalyser.php b/Task/Collect/CodeAnalyser.php index ea385042..49502898 100644 --- a/Task/Collect/CodeAnalyser.php +++ b/Task/Collect/CodeAnalyser.php @@ -113,20 +113,22 @@ protected function setupScript() { // This code is taken from DrupalKernel::attachSynthetic(). $container = $this->environment->getContainer(); $namespaces = $container->getParameter('container.namespaces'); + + // Build a list of data to pass to the script on STDIN. $psr4 = []; foreach ($namespaces as $prefix => $paths) { if (is_array($paths)) { foreach ($paths as $key => $value) { - $paths[$key] = $drupal_root . '/' . $value; + $path = $drupal_root . '/' . $value; + + $psr4[] = $prefix . '\\' . '::' . $path; } } elseif (is_string($paths)) { $paths = $drupal_root . '/' . $paths; - } - // Build a list of data to pass to the script on STDIN. - // $paths is never an array, AFAICT. - $psr4[] = $prefix . '\\' . '::' . $paths; + $psr4[] = $prefix . '\\' . '::' . $paths; + } } // Debug option for the script. diff --git a/Task/Collect/CollectorBase.php b/Task/Collect/CollectorBase.php index c179046e..8edf5f80 100644 --- a/Task/Collect/CollectorBase.php +++ b/Task/Collect/CollectorBase.php @@ -131,6 +131,25 @@ protected function findFiles(string $mask): array { return $files; } + /** + * Converts an absolute filepath into relative to the Drupal app root. + * + * @param string $filepath + * The absolute filepath. + * + * @return string + * The given filepath made relative to the Drupal app root, without an + * initial '/'. + */ + protected function makeFilepathRelative(string $filepath): string { + static $cwd; + if (!isset($cwd)) { + $cwd = getcwd(); + } + + return str_replace($cwd . '/', '', $filepath); + } + /** * Gets the first line from a docblock string. * diff --git a/Task/Collect/CollectorInterface.php b/Task/Collect/CollectorInterface.php index 0acced7e..8c85fccc 100644 --- a/Task/Collect/CollectorInterface.php +++ b/Task/Collect/CollectorInterface.php @@ -4,6 +4,9 @@ /** * Interface for collector task helpers. + * + * Task classes that implement this interface are automatically gathered and + * injected into the Collect task. */ interface CollectorInterface { diff --git a/Task/Collect/ContainerBuilderGetter.php b/Task/Collect/ContainerBuilderGetter.php index d4c5e5ed..17f552a8 100644 --- a/Task/Collect/ContainerBuilderGetter.php +++ b/Task/Collect/ContainerBuilderGetter.php @@ -27,7 +27,9 @@ public function getContainerBuilder() { $kernel_R = new \ReflectionClass($kernel); $compileContainer_R = $kernel_R->getMethod('compileContainer'); - $compileContainer_R->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $compileContainer_R->setAccessible(TRUE); + } $this->containerBuilder = $compileContainer_R->invoke($kernel); } diff --git a/Task/Collect/ElementTypesCollector.php b/Task/Collect/ElementTypesCollector.php index 0f0603f8..b993c8b8 100644 --- a/Task/Collect/ElementTypesCollector.php +++ b/Task/Collect/ElementTypesCollector.php @@ -50,6 +50,9 @@ public function getJobList() { * - 'type': The type ID. * - 'label': The label, which is the same as the type. * - 'form': Whether the element is a form input element. + * - class_filepath: The filepath to the element plugin's class. + * - description: A description of the plugin, taken from the plugin class + * docblock. */ public function collect($job_list) { $element_types = \Drupal::service('plugin.manager.element_info')->getDefinitions(); @@ -59,10 +62,21 @@ public function collect($job_list) { // We could use getInfo() on the plugin manager here, but it instantiates // each plugin which exhausts memory. $form = is_a($definition['class'], \Drupal\Core\Render\Element\FormElementInterface::class, TRUE); + + $plugin_class_reflection = new \ReflectionClass($definition['class']); + if ($docblock = $plugin_class_reflection->getDocComment()) { + $description = $this->getDocblockFirstLine($docblock); + } + else { + $description = ''; + } + $data[$id] = [ 'type' => $id, 'label' => $id, 'form' => $form, + 'class_filepath' => $this->makeFilepathRelative($plugin_class_reflection->getFileName()), + 'description' => $description, ]; } diff --git a/Task/Collect/EntityTypesCollector.php b/Task/Collect/EntityTypesCollector.php index da274208..3f78b9c6 100644 --- a/Task/Collect/EntityTypesCollector.php +++ b/Task/Collect/EntityTypesCollector.php @@ -61,8 +61,13 @@ public function collect($job_list) { $data = []; foreach ($entity_types as $id => $entity_type) { + $label = $entity_type->getLabel(); + if (is_object($label)) { + $label = $label->getUntranslatedString(); + } + $data[$id] = [ - 'label' => $entity_type->getLabel()->getUntranslatedString(), + 'label' => $label, 'group' => $entity_type->getGroup(), ]; diff --git a/Task/Collect/EventNamesCollector.php b/Task/Collect/EventNamesCollector.php index 032772f8..53a2e05d 100644 --- a/Task/Collect/EventNamesCollector.php +++ b/Task/Collect/EventNamesCollector.php @@ -53,6 +53,8 @@ public function collect($job_list) { // We can instantiate this just the once, because the keys of the $events // array have the fully-qualified class names. $visitor = new class extends NodeVisitorAbstract { + // Array keyed by qualified event constants, whose values are the docblock + // text or NULL if the event constant is missing documentation. public $events = []; public function enterNode(Node $node) { @@ -60,7 +62,7 @@ public function enterNode(Node $node) { $class_name = '\\' . $node->namespacedName->toString(); foreach ($node->getConstants() as $constant_node) { - $this->events[$class_name . '::' . $constant_node->consts[0]->name->name] = $constant_node->getDocComment()->getReformattedText(); + $this->events[$class_name . '::' . $constant_node->consts[0]->name->name] = $constant_node->getDocComment()?->getReformattedText(); } return NodeTraverser::STOP_TRAVERSAL; @@ -90,7 +92,14 @@ public function enterNode(Node $node) { } // Use just the first line of the docblock. - array_walk($visitor->events, fn (&$docblock) => $docblock = $this->getDocblockFirstLine($docblock)); + array_walk( + $visitor->events, + fn (&$docblock, $name) => $docblock = $docblock + // If there's a docblock, take the first line. + ? $this->getDocblockFirstLine($docblock) + // If there isn't, take the constant name. + : explode('::', $name)[1] + ); return $visitor->events; } diff --git a/Task/Collect/HooksCollector.php b/Task/Collect/HooksCollector.php index ad53e21a..d1c99244 100644 --- a/Task/Collect/HooksCollector.php +++ b/Task/Collect/HooksCollector.php @@ -101,8 +101,7 @@ public function mergeComponentData($existing_data, $new_data) { * Each item has the following properties: * - path: The full path to this file * - url: (internal to this handler) URL to download this file from. - * - original: (probably not used; just here for interest) the full path this - * file was copied from. + * - original: The full path this file was copied from. * - destination: The module code file where the hooks from this hook data * file should be saved by code generation. This may contain placeholders, * for instance, '%module.views.inc'. @@ -122,9 +121,7 @@ public function mergeComponentData($existing_data, $new_data) { * [module] => node * @endcode */ - protected function gatherHookDocumentationFiles($system_listing) { - // Needs to be overridden by subclasses. - } + abstract protected function gatherHookDocumentationFiles($system_listing); /** * Builds complete hook data array from downloaded files and stores in a file. @@ -255,7 +252,7 @@ protected function processHookData($hook_file_data) { // we can't call that because we need this information on lower versions of // core to properly generate forward-compatible hooks with the legacy // option. - $obligate_procedural_hooks = [ + $hooks_collector_pass_obligate_procedural_hooks = [ 'cache_flush', 'hook_info', 'install', @@ -270,10 +267,19 @@ protected function processHookData($hook_file_data) { 'update_last_removed', ]; + // These are not enforced by HookCollectorPass, but the hook + // documentation states they must be procedural. + $unenforced_obligate_procedural_hooks = [ + 'theme_suggestion_HOOK', + 'theme_suggestions_HOOK_alter', + ]; + + $obligate_procedural_hooks = array_merge($hooks_collector_pass_obligate_procedural_hooks, $unenforced_obligate_procedural_hooks); + $procedural = ( in_array($short_name, $obligate_procedural_hooks) || - preg_match('/^(post_update_|preprocess_|process_|update_\d+$)/', $short_name) + preg_match('/^(post_update_|process_|update_\d+$)/', $short_name) ); // Because we're working through the raw data array, we keep the incoming @@ -294,6 +300,9 @@ protected function processHookData($hook_file_data) { 'dependencies' => $hook_dependencies, 'group' => $group, 'core' => $file_data['core'] ?? NULL, + 'original_file_path' => !empty($file_data['original']) + ? $this->makeFilepathRelative($file_data['original']) + : NULL, 'file_path' => $file_data['path'], 'body' => $hook_data_raw['bodies'][$key], ]; diff --git a/Task/Collect/HooksCollector11.php b/Task/Collect/HooksCollector11.php index 2f074084..d2f8d87a 100644 --- a/Task/Collect/HooksCollector11.php +++ b/Task/Collect/HooksCollector11.php @@ -17,6 +17,8 @@ class HooksCollector11 extends HooksCollector { 'CORE_module.api.php' => TRUE, // Need this for hook_form_alter(). 'CORE_form.api.php' => TRUE, + // Need this for hook_ENTITY_TYPE_view(). + 'CORE_entity.api.php' => TRUE, // Need this for hook_tokens(). 'CORE_token.api.php' => TRUE, // Need this for hook_help(). @@ -105,7 +107,7 @@ public function getJobList() { */ protected function gatherHookDocumentationFiles($api_files) { // Get the hooks directory. - $directory = \DrupalCodeBuilder\Factory::getEnvironment()->getDataDirectory(); + $data_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getDataDirectory(); // Get Drupal root folder as a file path. // DRUPAL_ROOT is defined both by Drupal and Drush. @@ -136,7 +138,7 @@ protected function gatherHookDocumentationFiles($api_files) { $hook_files[$filename] = [ 'original' => $drupal_root . '/' . $file['uri'], // no idea if useful - 'path' => $directory . '/' . $file['filename'], + 'path' => $data_directory . '/' . $file['filename'], 'destination' => '%module.module', // Default. We override this below. 'group' => $file['group'], 'module' => $file['module'], @@ -241,6 +243,9 @@ protected function getAdditionalHookInfo() { 'hook_update_last_removed', 'hook_uninstall', ], + '%module.post_update.php' => [ + 'hook_post_update_NAME', + ], ], ], ]; diff --git a/Task/Collect/MetadataCollector.php b/Task/Collect/MetadataCollector.php new file mode 100644 index 00000000..825fa9b1 --- /dev/null +++ b/Task/Collect/MetadataCollector.php @@ -0,0 +1,45 @@ + time(), + ]; + } + +} diff --git a/Task/Collect/PluginTypesCollector.php b/Task/Collect/PluginTypesCollector.php index 53f75d2b..3ef1c592 100644 --- a/Task/Collect/PluginTypesCollector.php +++ b/Task/Collect/PluginTypesCollector.php @@ -224,6 +224,8 @@ protected function getPluginManagerServices() { * E.g., 'Plugin/Filter'. * - 'plugin_interface': The interface that plugin classes must implement, * as a qualified name (but without initial '\'). + * - 'plugin_interface_filepath': The filepath of the interface class, + * relative to the Drupal app root. * - 'plugin_definition_annotation_name': The class that the plugin * annotation uses, as a qualified name (but without initial '\'). * E.g, 'Drupal\filter\Annotation\Filter'. @@ -364,7 +366,9 @@ protected function addPluginTypeServiceData(&$data) { // Determine the alter hook name. if ($service_reflection->hasProperty('alterHook')) { $property_alter_hook = $service_reflection->getProperty('alterHook'); - $property_alter_hook->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property_alter_hook->setAccessible(TRUE); + } $alter_hook_name = $property_alter_hook->getValue($service); if (!empty($alter_hook_name)) { $data['alter_hook_name'] = $alter_hook_name . '_alter'; @@ -374,7 +378,9 @@ protected function addPluginTypeServiceData(&$data) { // Determine the plugin discovery type. // Get the discovery object from the plugin manager. $method_getDiscovery = $service_reflection->getMethod('getDiscovery'); - $method_getDiscovery->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $method_getDiscovery->setAccessible(TRUE); + } $discovery = $method_getDiscovery->invoke($service); $reflection_discovery = new \ReflectionClass($discovery); @@ -394,7 +400,10 @@ protected function addPluginTypeServiceData(&$data) { break; } - $property_decorated->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property_decorated->setAccessible(TRUE); + } + $decorated_discovery = $property_decorated->getValue($discovery); // We don't go in to a decorated class that's not in a Plugin component. @@ -473,6 +482,14 @@ protected function addPluginTypeServiceData(&$data) { $this->addPluginTypeServiceDataYaml($data, $service, $discovery); break; } + + // Add the file location of the interface file, to form an API link. + if ($data['plugin_interface']) { + $plugin_interface_reflection = new \ReflectionClass($data['plugin_interface']); + $interface_filepath = $plugin_interface_reflection->getFileName(); + + $data['plugin_interface_filepath'] = $this->makeFilepathRelative($interface_filepath); + } } /** @@ -515,7 +532,10 @@ protected function addPluginTypeServiceDataAttribute(&$data, $service, $discover } $property = $reflection->getProperty($property_name); - $property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property->setAccessible(TRUE); + } + $data[$data_key] = $property->getValue($service) ?? ''; } @@ -560,7 +580,10 @@ protected function addPluginTypeServiceDataAnnotated(&$data, $service, $discover } $property = $reflection->getProperty($property_name); - $property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property->setAccessible(TRUE); + } + $data[$data_key] = $property->getValue($service) ?? ''; } @@ -585,7 +608,10 @@ protected function addPluginTypeServiceDataAnnotated(&$data, $service, $discover protected function addPluginTypeServiceDataYaml(&$data, $service, $discovery) { $service_reflection = new \ReflectionClass($service); $property = $service_reflection->getProperty('defaults'); - $property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property->setAccessible(TRUE); + } + $defaults = $property->getValue($service); // YAML plugins don't specify their ID; it's generated automatically. @@ -598,12 +624,18 @@ protected function addPluginTypeServiceDataYaml(&$data, $service, $discovery) { // when recursively getting the decorated discovery. $discovery_reflection = new \ReflectionClass($discovery); $property = $discovery_reflection->getProperty('discovery'); - $property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property->setAccessible(TRUE); + } + $wrapped_discovery = $property->getValue($discovery); $wrapped_discovery_reflection = new \ReflectionClass($wrapped_discovery); $property = $wrapped_discovery_reflection->getProperty('name'); - $property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $property->setAccessible(TRUE); + } + $name = $property->getValue($wrapped_discovery); $data['yaml_file_suffix'] = $name; @@ -1482,7 +1514,10 @@ protected function addPluginModuleData(&$plugin_type_data) { // Unfortunately, there's no accessor for this, so some reflection hackery // is required until https://www.drupal.org/node/2907862 is fixed. $reflection = new \ReflectionProperty(\Drupal\plugin\PluginType\PluginType::class, 'pluginManagerServiceId'); - $reflection->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $reflection->setAccessible(TRUE); + } + foreach ($plugin_types as $plugin_type) { // Get the service ID from the reflection, and then our ID. diff --git a/Task/Collect/ServiceTagTypesCollector.php b/Task/Collect/ServiceTagTypesCollector.php index 5a9e94ae..57faba3e 100644 --- a/Task/Collect/ServiceTagTypesCollector.php +++ b/Task/Collect/ServiceTagTypesCollector.php @@ -77,6 +77,8 @@ public function getJobList() { * - 'service_id_collector: The collector has service IDs. * - 'interface': The fully-qualified name (without leading slash) of the * interface that each tagged service must implement. + * - interface_filepath: (optional) The filepath to the interface, relative + * to the Drupal app root. * - 'methods': An array of the methods of this interface, in the same * format as returned by MethodCollector::collectMethods(). */ @@ -94,6 +96,7 @@ public function collect($job_list) { 'interface' => 'Symfony\Component\EventDispatcher\EventSubscriberInterface', 'methods' => $this->methodCollector->collectMethods('Symfony\Component\EventDispatcher\EventSubscriberInterface'), // TODO: services of this type should go in the EventSubscriber namespace. + // No point adding the filepath, as api.d.o doesn't parse vendor code. ]; foreach ($collectors_info as $service_name => $tag_infos) { @@ -134,6 +137,13 @@ public function collect($job_list) { // Hope there's only one interface... $service_interfaces = $service_class_reflection->getInterfaceNames(); + + // ThemeNegotiatorInterface doesn't even behave this way, rabbithole + // for later. + if (empty($service_interfaces)) { + continue; + } + $collected_services_interface = array_shift($service_interfaces); if ($collected_services_interface) { @@ -169,10 +179,13 @@ public function collect($job_list) { $interface_methods = $this->methodCollector->collectMethods($collected_services_interface); } + $interface_reflection = new \ReflectionClass($collected_services_interface); + $data[$tag] = [ 'label' => $label, 'collector_type' => $collector_type, 'interface' => $collected_services_interface, + 'interface_filepath' => $this->makeFilepathRelative($interface_reflection->getFileName()), 'methods' => $interface_methods ?? [], ]; } diff --git a/Task/Collect/ServicesCollector.php b/Task/Collect/ServicesCollector.php index ae9cc9ba..5cce5e56 100644 --- a/Task/Collect/ServicesCollector.php +++ b/Task/Collect/ServicesCollector.php @@ -31,6 +31,7 @@ class ServicesCollector extends CollectorBase { 'module_handler' => TRUE, 'cache.discovery' => TRUE, 'storage:node' => TRUE, + 'Drupal\Core\DefaultContent\Importer' => TRUE, ]; /** @@ -251,7 +252,7 @@ protected function getAllServices(): array { $service_class = '\\' . $service_class; } - if (substr_count($service_id, '.') == 0) { + if (!str_contains($service_id, '.') && !str_contains($service_id, '\\')) { // If the service name does not contain any dots, in particular, // 'current_user', then use that, as it's usually clearer than the // class name. diff --git a/Task/Generate.php b/Task/Generate.php index 5330815f..d33d4161 100644 --- a/Task/Generate.php +++ b/Task/Generate.php @@ -111,7 +111,7 @@ public function getRootComponentData($component_type = 'module') { * @throws \DrupalCodeBuilder\Exception\InvalidInputException * Throws an exception if the given data is invalid. */ - public function generateComponent(DataItem $component_data, $existing_module_files = [], DataItem $configuration = NULL, DrupalExtension $existing_extension = NULL) { + public function generateComponent(DataItem $component_data, $existing_module_files = [], ?DataItem $configuration = NULL, ?DrupalExtension $existing_extension = NULL) { // Validate to ensure defaults are filled in. $component_data->validate(); diff --git a/Task/Generate/ComponentClassHandler.php b/Task/Generate/ComponentClassHandler.php index 5811df1e..1e909dbe 100644 --- a/Task/Generate/ComponentClassHandler.php +++ b/Task/Generate/ComponentClassHandler.php @@ -2,9 +2,12 @@ namespace DrupalCodeBuilder\Task\Generate; +use DI\Attribute\Inject; use DrupalCodeBuilder\Definition\MergingGeneratorDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; -use DI\Attribute\Inject; +use DrupalCodeBuilder\Environment\EnvironmentInterface; +use DrupalCodeBuilder\Generator\ClassHandlerAware; +use DrupalCodeBuilder\Generator\EnvironmentAware; /** * Task helper for working with generator classes and instantiating them. @@ -14,11 +17,22 @@ class ComponentClassHandler { /** * Constructor. * + * @param EnvironmentInterface $environment + * The environment object. * @param array $generator_classmap * The classmap of version-specific generator classes. Keys are the base - * class name, then the version, value is the short class name. + * class name, then the version, value is the short class name of the + * version-specific class. For example: + * @code + * [ + * 'AdminSettingsForm' => [ + * 7 => AdminSettingsForm7, + * ] + * ] + * @endcode */ public function __construct( + protected EnvironmentInterface $environment, #[Inject('generator_classmap')] protected array $generator_classmap ) { } @@ -40,7 +54,7 @@ public function __construct( * @throws \InvalidArgumentException * Throws an exception if there is no class found for the component type. */ - public function getStandaloneComponentPropertyDefinition(string $component_type, string $machine_name = NULL): PropertyDefinition { + public function getStandaloneComponentPropertyDefinition(string $component_type, ?string $machine_name = NULL): PropertyDefinition { $definition = MergingGeneratorDefinition::createFromGeneratorType($component_type); if (!$definition->getName()) { @@ -88,8 +102,15 @@ public function getGenerator($component_type, $component_data) { $generator = new $class($component_data, $this); - // Inject the class handler. - $generator->setClassHandler($this); + // Inject the class handler if needed. + if ($generator instanceof ClassHandlerAware) { + $generator->setClassHandler($this); + } + + // Inject the environment if needed. + if ($generator instanceof EnvironmentAware) { + $generator->setEnvironment($this->environment); + } return $generator; } diff --git a/Task/Generate/ComponentCollector.php b/Task/Generate/ComponentCollector.php index 3b89546a..cbf5f68f 100644 --- a/Task/Generate/ComponentCollector.php +++ b/Task/Generate/ComponentCollector.php @@ -174,7 +174,7 @@ public function __construct( * @return \DrupalCodeBuilder\Generator\Collection\ComponentCollection * The collection of components. */ - public function assembleComponentList(DataItem $component_data, DrupalExtension $extension = NULL): ComponentCollection { + public function assembleComponentList(DataItem $component_data, ?DrupalExtension $extension = NULL): ComponentCollection { // Reset all class properties. We don't normally run this twice, but // probably needed for tests. $this->requested_data_record = []; diff --git a/Task/Generate/FileAssembler.php b/Task/Generate/FileAssembler.php index f232274d..c2cb4b1d 100644 --- a/Task/Generate/FileAssembler.php +++ b/Task/Generate/FileAssembler.php @@ -23,7 +23,7 @@ class FileAssembler { * are filepaths relative to the module folder (eg, 'foo.module', * 'tests/module.test'). */ - public function generateFiles($component_data, ComponentCollection $component_collection, DrupalExtension $existing_extension = NULL) { + public function generateFiles($component_data, ComponentCollection $component_collection, ?DrupalExtension $existing_extension = NULL) { $component_list = $component_collection->getComponents(); // Let each file component in the tree gather data from its own children. @@ -73,7 +73,7 @@ protected function collectFileContents(ComponentCollection $component_collection * are filepaths relative to the module folder (eg, 'foo.module', * 'tests/module.test'). */ - protected function collectFiles(ComponentCollection $component_collection, DrupalExtension $existing_extension = NULL): array { + protected function collectFiles(ComponentCollection $component_collection, ?DrupalExtension $existing_extension = NULL): array { $code_files = []; // Components which provide a file should have registered themselves as diff --git a/Task/OptionsProviderTrait.php b/Task/OptionsProviderTrait.php index fb9bc9ac..ec9c087b 100644 --- a/Task/OptionsProviderTrait.php +++ b/Task/OptionsProviderTrait.php @@ -2,7 +2,7 @@ namespace DrupalCodeBuilder\Task; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; /** * Trait for \MutableTypedData\Definition\OptionSetDefininitionInterface. diff --git a/Task/ReportElementTypes.php b/Task/ReportElementTypes.php index 17c3ad38..70aa5ba4 100644 --- a/Task/ReportElementTypes.php +++ b/Task/ReportElementTypes.php @@ -3,13 +3,13 @@ namespace DrupalCodeBuilder\Task; use MutableTypedData\Definition\OptionSetDefininitionInterface; +use DrupalCodeBuilder\Definition\OptionDefinition; use DrupalCodeBuilder\Task\Report\SectionReportInterface; /** * Task handler for reporting on render element types. */ class ReportElementTypes extends ReportHookDataFolder implements OptionSetDefininitionInterface, SectionReportInterface { - use OptionsProviderTrait; use SectionReportSimpleCountTrait; protected $data; @@ -35,6 +35,29 @@ public function getInfo(): array { ]; } + /** + * {@inheritdoc} + */ + public function getOptions(): array { + if (!isset($this->data)) { + $this->data = $this->environment->getStorage()->retrieve($this->getInfo()['key']); + } + + $options = []; + foreach ($this->data as $id => $item) { + $url = $this->createClassLikeApiUrl($item['class_filepath'], 'class'); + + $options[$id] = OptionDefinition::create( + $id, + $item['label'], + description: $item['description'], + api_url: $url, + ); + } + + return $options; + } + /** * {@inheritdoc} */ diff --git a/Task/ReportHookClassMethodData.php b/Task/ReportHookClassMethodData.php new file mode 100644 index 00000000..f2f9e95c --- /dev/null +++ b/Task/ReportHookClassMethodData.php @@ -0,0 +1,48 @@ +listHookData(); + foreach ($data as $group => $hooks) { + foreach ($hooks as $key => $hook) { + // Skip an obligate procedural hook. + if (!empty($hook['procedural'])) { + continue; + } + + if ($hook['core'] && isset($hook['original_file_path'])) { + $url = 'https://api.drupal.org/api/drupal/' . + str_replace('/', '!', $hook['original_file_path']) . + '/function/' . + $hook['name'] . + '/' . $this->environment->getCoreMajorVersion(); + } + + $options[$hook['name']] = OptionDefinition::create( + $hook['name'], + $hook['name'], + description: $hook['description'] ?? '', + api_url: $url ?? NULL, + ); + } + } + + return $options; + } + +} diff --git a/Task/ReportHookData.php b/Task/ReportHookData.php index fd4cdbf9..59c9528e 100644 --- a/Task/ReportHookData.php +++ b/Task/ReportHookData.php @@ -7,8 +7,9 @@ namespace DrupalCodeBuilder\Task; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; use MutableTypedData\Definition\OptionSetDefininitionInterface; +use DrupalCodeBuilder\Definition\VariantMappingProviderInterface; use DrupalCodeBuilder\Task\Report\SectionReportInterface; /** @@ -16,13 +17,16 @@ * * TODO: revisit some of these and clean up names / clean up how many we have. */ -class ReportHookData extends ReportHookDataFolder implements OptionSetDefininitionInterface, SectionReportInterface { +class ReportHookData extends ReportHookDataFolder + implements OptionSetDefininitionInterface, VariantMappingProviderInterface, SectionReportInterface { /** * The sanity level this task requires to operate. */ protected $sanity_level = 'component_data_processed'; + protected $declarations; + /** * {@inheritdoc} */ @@ -122,10 +126,19 @@ public function getOptions(): array { $data = $this->listHookData(); foreach ($data as $group => $hooks) { foreach ($hooks as $key => $hook) { + if (!empty($hook['core']) && isset($hook['original_file_path'])) { + $url = 'https://api.drupal.org/api/drupal/' . + str_replace('/', '!', $hook['original_file_path']) . + '/function/' . + $hook['name'] . + '/' . $this->environment->getCoreMajorVersion(); + } + $options[$hook['name']] = OptionDefinition::create( $hook['name'], $hook['name'], - $hook['description'] ?? '' + description: $hook['description'] ?? '', + api_url: $url ?? NULL, ); } } @@ -133,6 +146,26 @@ public function getOptions(): array { return $options; } + /** + * {@inheritdoc} + */ + public function getVariantMapping(): array { + $mapping = []; + + $data = $this->listHookData(); + foreach ($data as $group => $hooks) { + foreach ($hooks as $key => $hook) { + $mapping[$key] = preg_match('@[[:upper:]]@', $key) ? 'tokenized' : 'literal'; + } + } + + // Special case for hook_update_N(): the uppercase 'N' is not a token, as + // it's derived automatically. + $mapping['hook_update_N'] = 'literal'; + + return $mapping; + } + /** * Get hooks as a list of options. * @@ -202,19 +235,21 @@ public function listHookOptionsStructured() { * - 'body': The hook function body, taken from the API file. */ function getHookDeclarations() { - $data = $this->listHookData(); + if (!isset($this->declarations)) { + $data = $this->listHookData(); - $return = []; - foreach ($data as $group => $hooks) { - foreach ($hooks as $key => $hook) { - // Standardize to lowercase. - $hook_name = strtolower($hook['name']); + $this->declarations = []; + foreach ($data as $group => $hooks) { + foreach ($hooks as $key => $hook) { + // Standardize to lowercase. + $hook_name = strtolower($hook['name']); - $return[$hook_name] = $hook; + $this->declarations[$hook_name] = $hook; + } } } - return $return; + return $this->declarations; } } diff --git a/Task/ReportHookDataFolder.php b/Task/ReportHookDataFolder.php index 8da8fb1e..37d62db2 100644 --- a/Task/ReportHookDataFolder.php +++ b/Task/ReportHookDataFolder.php @@ -27,6 +27,9 @@ class ReportHookDataFolder extends Base { * * @return * A unix timestamp, or NULL if the hooks have never been collected. + * + * @deprecated Use \DrupalCodeBuilder\Task\ReportSummary::lastUpdatedDate() + * instead. */ public function lastUpdatedDate() { $directory = $this->environment->getDataDirectory(); @@ -70,4 +73,25 @@ public function listHookFiles() { return $files; } + /** + * Gets a URL to api.d.o for a class-like. + * + * @param string $class_like_filepath + * The filepath to the class-like, relative to the Drupal app root. + * @param string $type + * The type, e.g. 'interface'. + * + * @return string + * A URL to the page on api.d.o for the given class-like, for the current + * major version of Drupal. + */ + protected function createClassLikeApiUrl(string $class_like_filepath, string $type): string { + return + 'https://api.drupal.org/api/drupal/' . + str_replace('/', '!', $class_like_filepath) . + "/$type/" . + basename($class_like_filepath, '.php') . + '/' . $this->environment->getCoreMajorVersion(); + } + } diff --git a/Task/ReportHookGroups.php b/Task/ReportHookGroups.php index 0883fff8..6f45d37d 100644 --- a/Task/ReportHookGroups.php +++ b/Task/ReportHookGroups.php @@ -7,7 +7,7 @@ namespace DrupalCodeBuilder\Task; -use MutableTypedData\Definition\OptionDefinition; +use DrupalCodeBuilder\Definition\OptionDefinition; use MutableTypedData\Definition\OptionSetDefininitionInterface; /** diff --git a/Task/ReportPluginData.php b/Task/ReportPluginData.php index fbcbef8f..da305b85 100644 --- a/Task/ReportPluginData.php +++ b/Task/ReportPluginData.php @@ -7,12 +7,13 @@ namespace DrupalCodeBuilder\Task; +use DrupalCodeBuilder\Definition\OptionDefinition; use MutableTypedData\Definition\OptionSetDefininitionInterface; use DrupalCodeBuilder\Definition\VariantMappingProviderInterface; use DrupalCodeBuilder\Task\Report\SectionReportInterface; /** - * Task handler for reporting on hook data. + * Task handler for reporting on plugin data. * * TODO: revisit some of these and clean up names / clean up how many we have. * Consider merging into a ReportComponentData Task. @@ -27,11 +28,6 @@ class ReportPluginData extends ReportHookDataFolder */ protected $sanity_level = 'component_data_processed'; - /** - * The name of the method providing an array of options as $value => $label. - */ - protected static $optionsMethod = 'listPluginNamesOptions'; - /** * Cached plugin type data. * @@ -78,17 +74,21 @@ public function getVariantMapping(): array { * The processed plugin data. * * @see \DrupalCodeBuilder\Task\Collect::gatherPluginTypeInfo() + * + * @todo Split this method into two. */ function listPluginData($discovery_type = NULL) { - // We may come here several times, so cache this. - // TODO: look into finer-grained caching higher up. - if (isset($this->cache[$discovery_type])) { - return $this->cache[$discovery_type]; + if ($discovery_type) { + // We may come here several times, so cache a filtered result. + // TODO: look into finer-grained caching higher up. + if (isset($this->cache[$discovery_type])) { + return $this->cache[$discovery_type]; + } } $plugin_data = $this->environment->getStorage()->retrieve('plugins'); - // Filter the plugins by the discovery type. + // Filter the plugins if there's a requested discovery type. if ($discovery_type) { $plugin_data = array_filter($plugin_data, function($item) use ($discovery_type) { $discovery_pieces = explode('\\', $item['discovery']); @@ -96,9 +96,9 @@ function listPluginData($discovery_type = NULL) { return ($discovery_short_name == $discovery_type); }); - } - $this->cache[$discovery_type] = $plugin_data; + $this->cache[$discovery_type] = $plugin_data; + } return $plugin_data; } @@ -123,6 +123,31 @@ public function listPluginDataBySubdirectory() { return $plugin_types_data_by_subdirectory; } + /** + * {@inheritdoc} + */ + public function getOptions(): array { + $data = $this->listPluginData(); + + $options = []; + foreach ($data as $plugin_type_name => $plugin_type_info) { + $url = NULL; + if (isset($plugin_type_info['plugin_interface_filepath'])) { + if (str_starts_with($plugin_type_info['plugin_interface_filepath'], 'core')) { + $url = $this->createClassLikeApiUrl($plugin_type_info['plugin_interface_filepath'], 'interface'); + } + } + + $options[$plugin_type_name] = OptionDefinition::create( + $plugin_type_name, + $plugin_type_info['type_label'], + api_url: $url ?? NULL, + ); + } + + return $options; + } + /** * Get plugin types as a list of options. * diff --git a/Task/ReportServiceTags.php b/Task/ReportServiceTags.php index 9789137e..5d3f13d8 100644 --- a/Task/ReportServiceTags.php +++ b/Task/ReportServiceTags.php @@ -28,6 +28,28 @@ public function getInfo(): array { ]; } + /** + * {@inheritdoc} + */ + public function listServiceTagData(): array { + if (!isset($this->data)) { + $this->data = $this->environment->getStorage()->retrieve('service_tag_types'); + } + + $data = []; + foreach ($this->data as $tag => $item) { + if (isset($item['interface_filepath'])) { + if (str_starts_with($item['interface_filepath'], 'core')) { + $item['api_url'] = $this->createClassLikeApiUrl($item['interface_filepath'], 'interface'); + } + } + + $data[$tag] = $item; + } + + return $data; + } + /** * {@inheritdoc} */ diff --git a/Task/ReportSummary.php b/Task/ReportSummary.php index 28a8cd80..1a8df762 100644 --- a/Task/ReportSummary.php +++ b/Task/ReportSummary.php @@ -45,6 +45,18 @@ public function setReportHelpers(array $helper_services) { $this->helperServices = $helper_services; } + /** + * Gets the timestamp of the last data analysis. + * + * @return + * A unix timestamp, or NULL if the data analysis has never been done. + */ + public function lastUpdatedDate(): ?int { + $metadata = $this->environment->getStorage()->retrieve('metadata'); + + return $metadata['timestamp'] ?? NULL; + } + /** * Returns a listing of all stored data, with counts. * diff --git a/Test/Constraint/CodeAdheresToCodingStandards.php b/Test/Constraint/CodeAdheresToCodingStandards.php index c33dbf50..b5b2c315 100644 --- a/Test/Constraint/CodeAdheresToCodingStandards.php +++ b/Test/Constraint/CodeAdheresToCodingStandards.php @@ -161,6 +161,11 @@ protected function setUpPHPCS($excluded_sniffs) { $runner->config->setConfigData('installed_paths', implode(',', [ static::$composerVendorDir . '/drupal/coder/coder_sniffer', static::$composerVendorDir . '/slevomat/coding-standard', + // Need to register our config data dir for both the case where this repo + // is the main project in CI testing, and the case where this repo is + // installed as a library in local development. PHPCS does not seem to + // mind a directory that doesn't exist. + getcwd() . '/Test/PHP_CodeSniffer', static::$composerVendorDir . '/drupal-code-builder/drupal-code-builder/Test/PHP_CodeSniffer', ])); diff --git a/Test/Fixtures/modules/test_analyze_9/src/Plugin/Action/Alpha.php b/Test/Fixtures/modules/test_analyze_9/src/Plugin/Action/Alpha.php index 42573a50..5e441986 100644 --- a/Test/Fixtures/modules/test_analyze_9/src/Plugin/Action/Alpha.php +++ b/Test/Fixtures/modules/test_analyze_9/src/Plugin/Action/Alpha.php @@ -76,7 +76,7 @@ public function executeMultiple(array $objects) { /** * {@inheritdoc} */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { // Checks object access. } diff --git a/Test/Integration/Collection/CollectPluginInfoDummyModulesTest.php b/Test/Integration/Collection/CollectPluginInfoDummyModulesTest.php index 3d89e430..3bb61a4d 100644 --- a/Test/Integration/Collection/CollectPluginInfoDummyModulesTest.php +++ b/Test/Integration/Collection/CollectPluginInfoDummyModulesTest.php @@ -35,7 +35,10 @@ protected function setUp(): void { // of plugin manager service IDs. $class = new \ReflectionObject($this->pluginTypesCollector); $this->gatherPluginTypeInfoMethod = $class->getMethod('gatherPluginTypeInfo'); - $this->gatherPluginTypeInfoMethod->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $this->gatherPluginTypeInfoMethod->setAccessible(TRUE); + } + } protected function getPluginTypeInfoFromCollector($job) { diff --git a/Test/Integration/Collection/CollectPluginInfoTest.php b/Test/Integration/Collection/CollectPluginInfoTest.php index aada724a..c98ca755 100644 --- a/Test/Integration/Collection/CollectPluginInfoTest.php +++ b/Test/Integration/Collection/CollectPluginInfoTest.php @@ -31,7 +31,10 @@ protected function setUp(): void { // of plugin manager service IDs. $class = new \ReflectionObject($this->pluginTypesCollector); $this->gatherPluginTypeInfoMethod = $class->getMethod('gatherPluginTypeInfo'); - $this->gatherPluginTypeInfoMethod->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $this->gatherPluginTypeInfoMethod->setAccessible(TRUE); + } + } /** diff --git a/Test/Integration/Installation/ContentEntityTypeTest.php b/Test/Integration/Installation/ContentEntityTypeTest.php index 4d7cf199..9c048b09 100644 --- a/Test/Integration/Installation/ContentEntityTypeTest.php +++ b/Test/Integration/Installation/ContentEntityTypeTest.php @@ -41,6 +41,7 @@ public function testSimpleContentEntityType() { 'entity_type_id' => 'kitty_cat', 'functionality' => [ 'owner', + 'fieldable', ], 'base_fields' => [ 0 => [ @@ -61,15 +62,20 @@ public function testSimpleContentEntityType() { $this->installModule($module_name); - // Get the entity type definition to check the entity class properly defines - // it. \Drupal::service('entity_type.manager')->clearCachedDefinitions(); + \Drupal::service('router.route_provider')->reset(); + // Get the entity type definition to check the entity class properly defines + // it. /** @var \Drupal\Core\Entity\EntityTypeInterface $definition */ $definition = \Drupal::service('entity_type.manager')->getDefinition('kitty_cat'); $this->assertIsObject($definition); $this->assertEquals('kitty_cat', $definition->id()); $this->assertEquals('Kitty Cat', $definition->getLabel()); + $this->assertEquals('entity.kitty_cat.settings', $definition->get('field_ui_base_route')); + + $route = \Drupal::service('router.route_provider')->getRouteByName('entity.kitty_cat.settings'); + $this->assertNotNull($route); } } diff --git a/Test/Integration/Installation/InstallationTestBase.php b/Test/Integration/Installation/InstallationTestBase.php index 7099e511..3e9d05cb 100644 --- a/Test/Integration/Installation/InstallationTestBase.php +++ b/Test/Integration/Installation/InstallationTestBase.php @@ -125,7 +125,9 @@ protected function getModuleParentFolderPath(): string { protected function writeModuleFiles($module_name, $files) { $module_folder = $this->getModuleParentFolderPath() . '/' . $module_name; - mkdir($module_folder, 0777, TRUE); + if (!file_exists($module_folder)) { + mkdir($module_folder, 0777, TRUE); + } foreach ($files as $filepath => $code) { $relative_file_dir = dirname($filepath); @@ -151,7 +153,10 @@ protected function installModule(string $module_name) { // ExtensionDiscovery keeps a cache of found files in a static property that // can only be cleared by hacking it with reflection. $reflection_property = new \ReflectionProperty(\Drupal\Core\Extension\ExtensionDiscovery::class, 'files'); - $reflection_property->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $reflection_property->setAccessible(TRUE); + } + $reflection_property->setValue(NULL, []); $result = $this->container->get('module_installer')->install([$module_name]); diff --git a/Test/Unit/ComponentAPI10Test.php b/Test/Unit/ComponentAPI10Test.php index 0773bd3f..0e2a2880 100644 --- a/Test/Unit/ComponentAPI10Test.php +++ b/Test/Unit/ComponentAPI10Test.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Task\AnalyzeModule; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; @@ -54,6 +55,7 @@ public function testModuleGenerationApiFile() { $php_tester->assertDrupalCodingStandards(); // TODO: expand the docblock assertion for these. + $this->assertStringNotContainsString('declare(strict_types=1);', $api_file); $this->assertStringContainsString("Hooks provided by the Test Module module.", $api_file, 'The API file contains the correct docblock header.'); $this->assertStringContainsString("@addtogroup hooks", $api_file, 'The API file contains the addtogroup docblock tag.'); $this->assertStringContainsString('@} End of "addtogroup hooks".', $api_file, 'The API file contains the closing addtogroup docblock tag.'); @@ -61,9 +63,8 @@ public function testModuleGenerationApiFile() { /** * Tests with an existing api file. - * - * @group existing */ + #[Group('existing')] public function testExistingAPIFile() { // Assemble module data. $module_name = 'test_module'; diff --git a/Test/Unit/ComponentAdminSettings10Test.php b/Test/Unit/ComponentAdminSettings10Test.php index a432255b..69e2b0b8 100644 --- a/Test/Unit/ComponentAdminSettings10Test.php +++ b/Test/Unit/ComponentAdminSettings10Test.php @@ -2,15 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests the AdminSettingsForm generator class. - * - * @group form - * @group yaml */ +#[Group('form')] +#[Group('yaml')] class ComponentAdminSettings10Test extends TestBase { /** @@ -61,20 +61,25 @@ function test8AdminSettingsGenerationTest() { $php_tester->assertHasClass('Drupal\test_module\Form\AdminSettingsForm'); $php_tester->assertClassHasParent('Drupal\Core\Form\ConfigFormBase'); + $php_tester->assertHasMethodOrder([ + 'getFormId', + 'getEditableConfigNames', + 'buildForm', + 'validateForm', + 'submitForm', + ]); + $method_tester = $php_tester->getMethodTester('getFormId'); $method_tester->getDocBlockTester()->assertHasInheritdoc(); $method_tester->assertReturnsString('test_module_settings_form'); - $form_builder_tester = $php_tester->getFormBuilderTester('buildForm', 1); - $form_builder_tester->assertElementCount(1); - - $php_tester->assertHasMethod('validateForm'); - $php_tester->assertHasMethod('submitForm'); - $method_tester = $php_tester->getMethodTester('getEditableConfigNames'); $method_tester->getDocBlockTester()->assertHasInheritdoc(); $method_tester->assertHasNoParameters(); + $form_builder_tester = $php_tester->getFormBuilderTester('buildForm', 1); + $form_builder_tester->assertElementCount(1); + // Check the schema file. $config_schema_file = $files['config/schema/test_module.schema.yml']; $yaml_tester = new YamlTester($config_schema_file); @@ -92,7 +97,7 @@ function test8AdminSettingsGenerationTest() { $yaml_tester->assertHasProperty($expected_route_name, "The routing file has the property for the admin route."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'path'], '/admin/config/system/test_module', "The routing file declares the route path."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_form'], '\Drupal\test_module\Form\AdminSettingsForm', "The routing file declares the route form."); - $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_title'], 'Administer test module', "The routing file declares the route title."); + $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_title'], 'Test Module settings', "The routing file declares the route title."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'requirements', '_permission'], 'administer test_module', "The routing file declares the route permission."); // Check the menu links file. @@ -203,7 +208,9 @@ function testAdminSettingsOtherRouterItemsTest() { $yaml_tester->assertHasProperty($expected_route_name, "The routing file has the property for the admin route."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'path'], '/admin/config/system/test_module', "The routing file declares the route path."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_form'], '\Drupal\test_module\Form\AdminSettingsForm', "The routing file declares the route form."); - $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_title'], 'Administer test module', "The routing file declares the route title."); + // This isn't title case because the module readable name isn't in title + // case. + $yaml_tester->assertPropertyHasValue([$expected_route_name, 'defaults', '_title'], 'Test module settings', "The routing file declares the route title."); $yaml_tester->assertPropertyHasValue([$expected_route_name, 'requirements', '_permission'], 'administer test_module', "The routing file declares the route permission."); $yaml_tester->assertPropertyHasValue(['test_module.requested.route.path', 'path'], "/requested/route/path", "The routing file declares the requested path."); diff --git a/Test/Unit/ComponentConfigEntityType10Test.php b/Test/Unit/ComponentConfigEntityType10Test.php index 3c1a9ed6..f61edd01 100644 --- a/Test/Unit/ComponentConfigEntityType10Test.php +++ b/Test/Unit/ComponentConfigEntityType10Test.php @@ -2,15 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests the config entity type generator class. - * - * @group yaml - * @group entity */ +#[Group('yaml')] +#[Group('entity')] class ComponentConfigEntityType10Test extends TestBase { /** @@ -340,10 +340,9 @@ public function testConfigEntityTypeWithHandlers() { /** * Test creating a config entity type with a UI. - * - * @group entity_ui - * @group form */ + #[Group('entity_ui')] + #[Group('form')] public function testConfigEntityTypeWithUI() { // Create a module. $module_name = 'test_module'; @@ -451,7 +450,7 @@ public function testConfigEntityTypeWithUI() { $yaml_tester = new YamlTester($action_links_file); $yaml_tester->assertHasProperty('entity.kitty_cat.add', 'The entity type has an add action link.'); - $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add Kitty Cat'); + $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add kitty cat'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'route_name'], 'entity.kitty_cat.add_form'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'appears_on'], ['entity.kitty_cat.collection']); diff --git a/Test/Unit/ComponentContentEntityType10Test.php b/Test/Unit/ComponentContentEntityType10Test.php index 87bf74fd..ddc9cd2d 100644 --- a/Test/Unit/ComponentContentEntityType10Test.php +++ b/Test/Unit/ComponentContentEntityType10Test.php @@ -2,16 +2,17 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests the entity type generator class. - * - * @group yaml - * @group annotation - * @group entity */ +#[Group('yaml')] +#[Group('annotation')] +#[Group('entity')] class ComponentContentEntityType10Test extends TestBase { /** @@ -63,6 +64,8 @@ public function testBasicContentEntityType() { 'src/Entity/KittyCat.php', 'src/Entity/KittyCatInterface.php', 'test_module.permissions.yml', + 'src/Controller/AdminStructureKittyCatController.php', + 'test_module.routing.yml', ], $files); $entity_interface_file = $files['src/Entity/KittyCatInterface.php']; @@ -139,7 +142,7 @@ public function testBasicContentEntityType() { $annotation_tester->assertPropertyHasValue('base_table', 'kitty_cat'); $annotation_tester->assertPropertyHasValue(['handlers', 'list_builder'], 'Drupal\Core\Entity\EntityListBuilder'); $annotation_tester->assertPropertyHasValue('admin_permission', 'administer kitty_cat entities'); - $annotation_tester->assertPropertyHasValue('field_ui_base_route', 'entity.kitty_cat.admin_form'); + $annotation_tester->assertPropertyHasValue('field_ui_base_route', 'entity.kitty_cat.settings'); $annotation_tester->assertHasProperties([ 'id', 'label', @@ -171,9 +174,8 @@ public function testBasicContentEntityType() { * Tests the parent interfaces. * * This covers the different combinations of options. - * - * @dataProvider providerContentEntityTypeFunctionalityOptions */ + #[DataProvider('providerContentEntityTypeFunctionalityOptions')] public function testContentEntityTypeFunctionalityOptions( $interface_option, $expected_parent_interfaces, @@ -401,6 +403,8 @@ public function testEntityTypeWithTranslation() { 'src/Entity/KittyCat.php', 'src/Entity/KittyCatInterface.php', 'test_module.permissions.yml', + 'src/Controller/AdminStructureKittyCatController.php', + 'test_module.routing.yml', ], $files); $entity_class_file = $files['src/Entity/KittyCat.php']; @@ -524,6 +528,8 @@ public function testEntityTypeWithRevisions() { 'src/Entity/KittyCat.php', 'src/Entity/KittyCatInterface.php', 'test_module.permissions.yml', + 'src/Controller/AdminStructureKittyCatController.php', + 'test_module.routing.yml', ], $files); $entity_class_file = $files['src/Entity/KittyCat.php']; @@ -588,6 +594,7 @@ public function testEntityTypeWithRevisions() { 'handlers', 'admin_permission', 'entity_keys', + 'revision_metadata_keys', 'field_ui_base_route', ]); $annotation_tester->assertPropertyHasValue('base_table', 'kitty_cat'); @@ -600,6 +607,10 @@ public function testEntityTypeWithRevisions() { ], 'entity_keys'); $annotation_tester->assertPropertyHasValue(['entity_keys', 'id'], 'kitty_cat_id'); $annotation_tester->assertPropertyHasValue(['entity_keys', 'revision'], 'revision_id'); + + $annotation_tester->assertPropertyHasValue(['revision_metadata_keys', 'revision_user'], 'revision_uid'); + $annotation_tester->assertPropertyHasValue(['revision_metadata_keys', 'revision_created'], 'revision_timestamp'); + $annotation_tester->assertPropertyHasValue(['revision_metadata_keys', 'revision_log_message'], 'revision_log'); } /** @@ -769,9 +780,8 @@ public function testEntityTypeWithBundleEntityNoUI() { * Tests creating a content entity type with handlers. * * This covers the different combinations of options. - * - * @dataProvider providerHandlers */ + #[DataProvider('providerHandlers')] public function testContentEntityTypeHandlers($handler_properties, $expected_handlers_annotation, $expected_files_base_classes) { // Create a module. $module_name = 'test_module'; @@ -1310,9 +1320,8 @@ public function testContentEntityTypeCustomHandlers() { /** * Tests the handler namespace configuration setting. - * - * @group config */ + #[Group('config')] public function testContentEntityTypeHandlerNamespaceConfiguration() { // Create a module. $module_name = 'test_module'; @@ -1385,10 +1394,9 @@ public function testContentEntityTypeHandlerNamespaceConfiguration() { /** * Tests creating a content entity with a UI. - * - * @group entity_ui - * @group form */ + #[Group('entity_ui')] + #[Group('form')] public function testContentEntityTypeWithUI() { $module_name = 'test_module'; $module_data = [ @@ -1425,9 +1433,18 @@ public function testContentEntityTypeWithUI() { 'label_plural', 'label_count', 'base_table', + // The next 4 lines are here only as a workaround for an MTD bug: see + // https://github.com/joachim-n/mutable-typed-data/issues/22. + 'data_table', + 'revision_table', + 'revision_data_table', + 'translatable', 'handlers', 'admin_permission', 'entity_keys', + // Next 2 lines same as above. + 'revision_metadata_keys', + 'field_ui_base_route', 'links', ]); $annotation_tester->assertPropertyHasValue(['handlers', 'form', 'default'], 'Drupal\test_module\Form\KittyCatForm'); @@ -1465,7 +1482,7 @@ public function testContentEntityTypeWithUI() { $yaml_tester = new YamlTester($action_links_file); $yaml_tester->assertHasProperty('entity.kitty_cat.add', 'The content entity type has an add action link.'); - $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add Kitty Cat'); + $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add kitty cat'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'route_name'], 'entity.kitty_cat.add_form', "The route for adding a content entity is for the add form."); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'appears_on'], ['entity.kitty_cat.collection']); @@ -1493,12 +1510,51 @@ public function testContentEntityTypeWithUI() { $save_method_tester->assertHasLine('$form_state->setRedirectUrl($this->entity->toUrl(\'canonical\'));'); } + /** + * Tests creating a content entity with a revision UI. + */ + #[Group('entity_ui')] + #[Group('form')] + public function testContentEntityTypeWithRevisionEntityUI() { + $module_name = 'test_module'; + $module_data = [ + 'base' => 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test module', + 'content_entity_types' => [ + 0 => [ + 'entity_type_id' => 'kitty_cat', + 'functionality' => [ + 'revisionable', + ], + 'entity_ui' => 'admin', + ], + ], + 'readme' => FALSE, + ]; + + $files = $this->generateModuleFiles($module_data); + + $entity_class_file = $files['src/Entity/KittyCat.php']; + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $entity_class_file); + $annotation_tester = $php_tester->getAnnotationTesterForClass(); + + $annotation_tester->assertPropertyHasValue(['handlers', 'route_provider', 'revision'], 'Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider'); + + $annotation_tester->assertPropertyHasValue(['handlers', 'form', 'revision-delete'], 'Drupal\Core\Entity\Form\RevisionDeleteForm'); + $annotation_tester->assertPropertyHasValue(['handlers', 'form', 'revision-revert'], 'Drupal\Core\Entity\Form\RevisionRevertForm'); + + $annotation_tester->assertPropertyHasValue(['links', 'revision'], '/kitty_cat/{kitty_cat}/revisions/{kitty_cat_revision}/view'); + $annotation_tester->assertPropertyHasValue(['links', 'revision-delete-form'], '/kitty_cat/{kitty_cat}/revisions/{kitty_cat_revision}/view'); + $annotation_tester->assertPropertyHasValue(['links', 'revision-revert-form'], '/kitty_cat/{kitty_cat}/revisions/{kitty_cat_revision}/revert'); + $annotation_tester->assertPropertyHasValue(['links', 'version-history'], '/kitty_cat/{kitty_cat}/revisions'); + } + /** * Tests creating a content entity with a bundle entity UI. - * - * @group entity_ui - * @group form */ + #[Group('entity_ui')] + #[Group('form')] public function testContentEntityTypeWithBundleEntityUI() { $module_name = 'test_module'; $module_data = [ @@ -1570,10 +1626,9 @@ public function testContentEntityTypeWithBundleEntityUI() { /** * Test creating a content entity type with a bundle entity and UI for both. - * - * @group entity_ui - * @group form */ + #[Group('entity_ui')] + #[Group('form')] public function testEntityTypeWithUIAndBundleEntity() { // Create a module. $module_name = 'test_module'; @@ -1640,10 +1695,20 @@ public function testEntityTypeWithUIAndBundleEntity() { 'label_count', 'bundle_label', 'base_table', + // The next 4 lines are here only as a workaround for an MTD bug: see + // https://github.com/joachim-n/mutable-typed-data/issues/22. + 'data_table', + 'revision_table', + 'revision_data_table', + 'translatable', 'handlers', 'admin_permission', 'entity_keys', + // Next line same as above. + 'revision_metadata_keys', 'bundle_entity_type', + // Next line same as above. + 'field_ui_base_route', 'links', ]); $annotation_tester->assertPropertyHasValue('bundle_label', "Kitty Cat Type"); @@ -1695,11 +1760,11 @@ public function testEntityTypeWithUIAndBundleEntity() { $yaml_tester = new YamlTester($action_links_file); $yaml_tester->assertHasProperty('entity.kitty_cat_type.add', 'The bundle entity type has an add action link.'); - $yaml_tester->assertPropertyHasValue(['entity.kitty_cat_type.add', 'title'], 'Add Kitty Cat Type'); + $yaml_tester->assertPropertyHasValue(['entity.kitty_cat_type.add', 'title'], 'Add kitty cat type'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat_type.add', 'route_name'], 'entity.kitty_cat_type.add_form'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat_type.add', 'appears_on'], ['entity.kitty_cat_type.collection']); $yaml_tester->assertHasProperty('entity.kitty_cat.add', 'The content entity type has an add action link.'); - $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add Kitty Cat'); + $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'title'], 'Add kitty cat'); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'route_name'], 'entity.kitty_cat.add_page', "The route for adding a content entity is for the add page, rather than the add form."); $yaml_tester->assertPropertyHasValue(['entity.kitty_cat.add', 'appears_on'], ['entity.kitty_cat.collection']); diff --git a/Test/Unit/ComponentContentEntityType9Test.php b/Test/Unit/ComponentContentEntityType9Test.php new file mode 100644 index 00000000..6e634044 --- /dev/null +++ b/Test/Unit/ComponentContentEntityType9Test.php @@ -0,0 +1,56 @@ + 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test module', + 'content_entity_types' => [ + 0 => [ + 'entity_type_id' => 'kitty_cat', + 'functionality' => [ + 'revisionable', + ], + 'entity_ui' => 'admin', + ], + ], + 'readme' => FALSE, + ]; + + $files = $this->generateModuleFiles($module_data); + + $entity_class_file = $files['src/Entity/KittyCat.php']; + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $entity_class_file); + $annotation_tester = $php_tester->getAnnotationTesterForClass(); + + $annotation_tester->assertNotHasProperty(['handlers', 'route_provider', 'revision']); + } + +} diff --git a/Test/Unit/ComponentCssLibrary10Test.php b/Test/Unit/ComponentCssLibrary10Test.php index fdcb1d33..8c8a8ecd 100644 --- a/Test/Unit/ComponentCssLibrary10Test.php +++ b/Test/Unit/ComponentCssLibrary10Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for library generation. - * - * @group yaml */ +#[Group('yaml')] class ComponentCssLibrary10Test extends TestBase { /** diff --git a/Test/Unit/ComponentDrushCommand11Test.php b/Test/Unit/ComponentDrushCommand11Test.php index 2b436e71..31d50473 100644 --- a/Test/Unit/ComponentDrushCommand11Test.php +++ b/Test/Unit/ComponentDrushCommand11Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests for Drush command component. - * - * @group yaml */ +#[Group('yaml')] class ComponentDrushCommand11Test extends TestBase { /** @@ -46,14 +46,14 @@ public function testBasicCommandGeneration() { $this->assertFiles([ 'test_module.info.yml', - 'src/Commands/TestModuleCommands.php', + 'src/Drush/Commands/TestModuleCommands.php', ], $files); - $command_class_file = $files["src/Commands/TestModuleCommands.php"]; + $command_class_file = $files["src/Drush/Commands/TestModuleCommands.php"]; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $command_class_file); $php_tester->assertDrupalCodingStandards(); - $php_tester->assertHasClass('Drupal\test_module\Commands\TestModuleCommands'); + $php_tester->assertHasClass('Drupal\test_module\Drush\Commands\TestModuleCommands'); $php_tester->assertClassHasParent('Drush\Commands\DrushCommands'); $php_tester->getClassDocBlockTester()->assertHasLine('Test module Drush commands.'); $php_tester->assertHasMethod('alpha'); @@ -111,10 +111,10 @@ function testCommandGenerationWithParameters() { $this->assertFiles([ 'test_module.info.yml', - 'src/Commands/TestModuleCommands.php', + 'src/Drush/Commands/TestModuleCommands.php', ], $files); - $command_class_file = $files["src/Commands/TestModuleCommands.php"]; + $command_class_file = $files["src/Drush/Commands/TestModuleCommands.php"]; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $command_class_file); $php_tester->assertDrupalCodingStandards([ @@ -122,7 +122,7 @@ function testCommandGenerationWithParameters() { // See https://www.drupal.org/project/coder/issues/3475912. 'Drupal.Arrays.Array.LongLineDeclaration', ]); - $php_tester->assertHasClass('Drupal\test_module\Commands\TestModuleCommands'); + $php_tester->assertHasClass('Drupal\test_module\Drush\Commands\TestModuleCommands'); $php_tester->assertClassHasParent('Drush\Commands\DrushCommands'); $php_tester->getClassDocBlockTester()->assertHasLine('Test module Drush commands.'); $php_tester->assertHasMethod('alpha'); @@ -152,9 +152,8 @@ function testCommandGenerationWithParameters() { /** * Test a command with with injected services. - * - * @group di */ + #[Group('di')] function testCommandGenerationWithServices() { // Assemble module data. $module_name = 'test_module'; @@ -186,15 +185,16 @@ function testCommandGenerationWithServices() { $this->assertFiles([ 'test_module.info.yml', - 'src/Commands/TestModuleCommands.php', + 'src/Drush/Commands/TestModuleCommands.php', ], $files); - $command_class_file = $files["src/Commands/TestModuleCommands.php"]; + $command_class_file = $files["src/Drush/Commands/TestModuleCommands.php"]; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $command_class_file); $php_tester->assertDrupalCodingStandards(); - $php_tester->assertHasClass('Drupal\test_module\Commands\TestModuleCommands'); + $php_tester->assertHasClass('Drupal\test_module\Drush\Commands\TestModuleCommands'); $php_tester->assertClassHasParent('Drush\Commands\DrushCommands'); + $php_tester->assertNotClassHasInterfaces(['Drupal\Core\DependencyInjection\ContainerInjectionInterface']); $php_tester->getClassDocBlockTester()->assertHasLine('Test module Drush commands.'); $php_tester->assertHasMethod('alpha'); $php_tester->assertHasMethod('beta'); @@ -254,14 +254,14 @@ public function testCommandGenerationWithInflection() { $this->assertFiles([ 'test_module.info.yml', - 'src/Commands/TestModuleCommands.php', + 'src/Drush/Commands/TestModuleCommands.php', ], $files); - $command_class_file = $files["src/Commands/TestModuleCommands.php"]; + $command_class_file = $files["src/Drush/Commands/TestModuleCommands.php"]; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $command_class_file); $php_tester->assertDrupalCodingStandards(); - $php_tester->assertHasClass('Drupal\test_module\Commands\TestModuleCommands'); + $php_tester->assertHasClass('Drupal\test_module\Drush\Commands\TestModuleCommands'); $php_tester->assertClassHasParent('Drush\Commands\DrushCommands'); $php_tester->getClassDocBlockTester()->assertHasLine('Test module Drush commands.'); $php_tester->assertHasMethod('alpha'); diff --git a/Test/Unit/ComponentDrushCommand8Test.php b/Test/Unit/ComponentDrushCommand8Test.php index 873312fa..dc941115 100644 --- a/Test/Unit/ComponentDrushCommand8Test.php +++ b/Test/Unit/ComponentDrushCommand8Test.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Drush command component. - * - * @group yaml */ +#[Group('yaml')] class ComponentDrushCommand8Test extends TestBase { /** @@ -173,9 +173,8 @@ function testCommandGenerationWithParameters() { /** * Test a command with with injected services. - * - * @group di */ + #[Group('di')] function testCommandGenerationWithServices() { // Assemble module data. $module_name = 'test_module'; diff --git a/Test/Unit/ComponentRouteCallback10Test.php b/Test/Unit/ComponentDynamicRouteProvider11Test.php similarity index 85% rename from Test/Unit/ComponentRouteCallback10Test.php rename to Test/Unit/ComponentDynamicRouteProvider11Test.php index 1b3c4c2a..a01edf05 100644 --- a/Test/Unit/ComponentRouteCallback10Test.php +++ b/Test/Unit/ComponentDynamicRouteProvider11Test.php @@ -2,35 +2,35 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for dynamic route providers. - * - * @group yaml */ -class ComponentRouteCallback10Test extends TestBase { +#[Group('yaml')] +class ComponentDynamicRouteProvider11Test extends TestBase { /** * {@inheritdoc} */ - protected $drupalMajorVersion = 9; + protected $drupalMajorVersion = 11; /** * Test generating a module info file. */ - public function testRouteCallback() { + public function testDynamicRouteProvider() { $module_data = [ 'base' => 'module', 'root_name' => 'test_module', 'readable_name' => 'Test Module', 'dynamic_routes' => [ 0 => [ - 'provider_class_short_name' => 'MyRouteProvider', + 'plain_class_name' => 'MyRouteProvider', ], 1 => [ - 'provider_class_short_name' => 'OtherRouteProvider', + 'plain_class_name' => 'OtherRouteProvider', ], ], 'readme' => FALSE, diff --git a/Test/Unit/ComponentForm10Test.php b/Test/Unit/ComponentForm10Test.php index f8d8c0b5..8125e507 100644 --- a/Test/Unit/ComponentForm10Test.php +++ b/Test/Unit/ComponentForm10Test.php @@ -2,14 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Form component. - * - * @group form */ +#[Group('form')] class ComponentForm10Test extends TestBase { /** @@ -129,9 +130,8 @@ public function testFormGenerationWithCustomElements() { /** * Test Form component with injected services. - * - * @group di */ + #[Group('di')] function testFormGenerationWithServices() { // Assemble module data. $module_name = 'test_module'; @@ -255,10 +255,9 @@ public static function dataAdoptionMerge() { * Whether a generated component exists to have the adopted component merged * with. * - * @group adopt - * - * @dataProvider dataAdoptionMerge */ + #[Group('adopt')] + #[DataProvider('dataAdoptionMerge')] public function testExistingFormAdoption(bool $merge) { // First pass: generate the files we'll mock as existing. $module_name = 'existing'; diff --git a/Test/Unit/ComponentHooks11Test.php b/Test/Unit/ComponentHooks11Test.php index 57fab9da..3076cb21 100644 --- a/Test/Unit/ComponentHooks11Test.php +++ b/Test/Unit/ComponentHooks11Test.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Hooks component on Drupal 11. - * - * @group hooks */ +#[Group('hooks')] class ComponentHooks11Test extends TestBase { /** @@ -19,6 +19,190 @@ class ComponentHooks11Test extends TestBase { */ protected $drupalMajorVersion = 11; + /** + * Tests hooks in a custom class, without legacy support. + */ + public function testHookClassesOnlyOO(): void { + $module_name = 'test_module'; + $module_data = [ + 'base' => 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test Module', + 'short_description' => 'Test Module description', + 'hook_implementation_type' => 'oo', + + 'hook_classes' => [ + 0 => [ + // Don't give a hooks class name to test the incrementing default. + 'injected_services' => [ + 'current_user', + 'entity_type.manager', + ], + 'hook_methods' => [ + 0 => [ + 'hook_name' => 'hook_form_alter', + ], + 1 => [ + 'hook_name' => 'hook_form_FORM_ID_alter', + 'hook_name_parameters' => [ + 'node_form', + ], + ], + 2 => [ + 'hook_name' => 'hook_form_FORM_ID_alter', + 'hook_name_parameters' => [ + 'user_form', + ], + ], + 3 => [ + 'hook_name' => 'hook_ENTITY_TYPE_view', + 'hook_name_parameters' => [ + 'node', + ], + ], + ], + ], + 1 => [ + 'hook_methods' => [ + 0 => [ + 'hook_name' => 'hook_form_alter', + ], + ], + ], + ], + 'readme' => FALSE, + ]; + + $files = $this->generateModuleFiles($module_data); + + $this->assertFiles([ + 'test_module.info.yml', + 'src/Hook/TestModuleHooks.php', + 'src/Hook/TestModuleHooks2.php', + ], $files); + + $hooks_file = $files['src/Hook/TestModuleHooks.php']; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $hooks_file); + $php_tester->assertDrupalCodingStandards([ + // Temporarily exclude the sniff for comment lines being too long, as a + // comment in hook_form_alter() violates this. + // TODO: remove this when https://www.drupal.org/project/drupal/issues/2924184 + // is fixed. + 'Drupal.Files.LineLength.TooLong', + // Sample code for hook_form_FORM_ID_alter() violates this. + 'Drupal.Commenting.InlineComment.SpacingAfter', + ]); + + $php_tester->assertHasClass('Drupal\test_module\Hook\TestModuleHooks'); + $php_tester->getMethodTester('formAlter') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['form_alter']); + $php_tester->getMethodTester('formNodeFormAlter') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['form_node_form_alter']); + $php_tester->getMethodTester('formUserFormAlter') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['form_user_form_alter']); + $php_tester->getMethodTester('nodeView') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['node_view']); + } + + /** + * Tests hooks in a custom class, with legacy support. + */ + public function testHookClassesWithLegacy(): void { + $module_name = 'test_module'; + $module_data = [ + 'base' => 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test Module', + 'short_description' => 'Test Module description', + 'hook_implementation_type' => 'oo_legacy', + 'hook_classes' => [ + 0 => [ + 'plain_class_name' => 'AlphaHooks', + 'injected_services' => [ + 'current_user', + 'entity_type.manager', + ], + 'hook_methods' => [ + 0 => [ + 'hook_name' => 'hook_form_alter', + ], + 1 => [ + 'hook_name' => 'hook_form_FORM_ID_alter', + 'hook_name_parameters' => [ + 'node_form', + ], + ], + 2 => [ + 'hook_name' => 'hook_form_FORM_ID_alter', + 'hook_name_parameters' => [ + 'user_form', + ], + ], + 3 => [ + 'hook_name' => 'hook_ENTITY_TYPE_view', + 'hook_name_parameters' => [ + 'node', + ], + ], + ], + ], + ], + 'readme' => FALSE, + ]; + + $files = $this->generateModuleFiles($module_data); + + $this->assertFiles([ + 'test_module.info.yml', + 'test_module.module', + 'test_module.services.yml', + 'src/Hook/AlphaHooks.php', + ], $files); + + $hooks_file = $files['src/Hook/AlphaHooks.php']; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $hooks_file); + $php_tester->assertDrupalCodingStandards([ + // Temporarily exclude the sniff for comment lines being too long, as a + // comment in hook_form_alter() violates this. + // TODO: remove this when https://www.drupal.org/project/drupal/issues/2924184 + // is fixed. + 'Drupal.Files.LineLength.TooLong', + // Sample code for hook_form_FORM_ID_alter() violates this. + 'Drupal.Commenting.InlineComment.SpacingAfter', + ]); + + $php_tester->assertHasClass('Drupal\test_module\Hook\AlphaHooks'); + $php_tester->getMethodTester('formAlter') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['form_alter']); + + $module_file = $files['test_module.module']; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $module_file); + $php_tester->assertDrupalCodingStandards([ + // Generated method name for hook with name tokens violates this. + 'Drupal.NamingConventions.ValidFunctionName.InvalidName', + ]); + $php_tester->assertFileDocblockHasLine("Contains hook implementations for the Test Module module."); + + $function_tester = $php_tester->getFunctionTester('test_module_form_alter'); + $function_tester->getDocBlockTester()->assertHasLine('Legacy hook implementation.'); + $function_tester->assertHasLine('\Drupal::service(AlphaHooks::class)->formAlter($form, $form_state, $form_id);'); + + $function_tester = $php_tester->getFunctionTester('test_module_form_node_form_alter'); + $function_tester->getDocBlockTester()->assertHasLine('Legacy hook implementation.'); + $function_tester->assertHasLine('\Drupal::service(AlphaHooks::class)->formNodeFormAlter($form, $form_state, $form_id);'); + + $function_tester = $php_tester->getFunctionTester('test_module_form_user_form_alter'); + $function_tester->getDocBlockTester()->assertHasLine('Legacy hook implementation.'); + $function_tester->assertHasLine('\Drupal::service(AlphaHooks::class)->formUserFormAlter($form, $form_state, $form_id);'); + + $function_tester = $php_tester->getFunctionTester('test_module_node_view'); + $function_tester->getDocBlockTester()->assertHasLine('Legacy hook implementation.'); + $function_tester->assertHasLine('\Drupal::service(AlphaHooks::class)->nodeView($build, $entity, $display, $view_mode);'); + } + /** * Tests procedural hooks can also be generated on 11. */ @@ -86,14 +270,12 @@ public function testOOHooks() { ]); $php_tester->assertHasClass('Drupal\test_module\Hook\TestModuleHooks'); - $php_tester->assertHasMethod('formAlter'); - $php_tester->assertHasMethod('blockAccess'); + $php_tester->getMethodTester('formAlter') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['form_alter']); + $php_tester->getMethodTester('blockAccess') + ->assertHasAttribute('\Drupal\Core\Hook\Attribute\Hook', ['block_access']); $php_tester->assertNotHasMethod('install'); - // TODO: Attribute testing. - $this->assertStringContainsString("#[Hook('form_alter')]", $hooks_file); - $this->assertStringContainsString("#[Hook('block_access')]", $hooks_file); - // Check the .install file has a procedural implementation for // hook_install(). $install_file = $files['test_module.install']; @@ -215,6 +397,46 @@ public function testHookImplementationLegacy() { $function_tester->assertHasLine('\Drupal::service(TestModuleHooks::class)->blockViewAlter($build, $block);'); } + /** + * Tests generation of legacy hooks with an explicit hooks class of same name. + */ + public function testHookImplementationLegacyWithSameHooksClass() { + $module_name = 'test_module'; + $module_data = [ + 'base' => 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test Module', + 'short_description' => 'Test Module description', + 'hook_implementation_type' => 'oo_legacy', + 'hooks' => [ + 'hook_block_access', + 'hook_block_view_alter', + ], + 'hook_classes' => [ + 0 => [ + // Hooks class name is the same as the one that will be generated + // automatically. + 'plain_class_name' => 'TestModuleHooks', + 'hook_methods' => [ + 0 => [ + 'hook_name' => 'hook_form_alter', + ], + ], + ], + ], + 'readme' => FALSE, + ]; + + $files = $this->generateModuleFiles($module_data); + + $this->assertFiles([ + 'test_module.info.yml', + 'test_module.module', + 'test_module.services.yml', + 'src/Hook/TestModuleHooks.php', + ], $files); + } + /** * Tests generation of legacy hooks merges with other generated services. */ diff --git a/Test/Unit/ComponentHooks7Test.php b/Test/Unit/ComponentHooks7Test.php index 024c2565..1ac3d596 100644 --- a/Test/Unit/ComponentHooks7Test.php +++ b/Test/Unit/ComponentHooks7Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests for Hooks component. - * - * @group hooks */ +#[Group('hooks')] class ComponentHooks7Test extends TestBase { /** diff --git a/Test/Unit/ComponentHooks8Test.php b/Test/Unit/ComponentHooks8Test.php index 10ce5b5d..7f3adfca 100644 --- a/Test/Unit/ComponentHooks8Test.php +++ b/Test/Unit/ComponentHooks8Test.php @@ -2,15 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Hooks component. - * - * @group hooks */ +#[Group('hooks')] class ComponentHooks8Test extends TestBase { /** @@ -173,9 +173,8 @@ public function testModuleGenerationHooks() { /** * Tests with an existing code file. - * - * @group existing */ + #[Group('existing')] public function testHooksWithExistingFunctions() { // Assemble module data. $module_name = 'test_module'; @@ -255,9 +254,8 @@ function test_module_element_info_alter(array &$info) { /** * Tests hook_update_N() existing implementations. - * - * @group existing */ + #[Group('existing')] public function testHooksWithExistingHookInstall() { // Assemble module data. $module_name = 'test_module'; diff --git a/Test/Unit/ComponentInfo10Test.php b/Test/Unit/ComponentInfo10Test.php index 26cff7e3..6c039954 100644 --- a/Test/Unit/ComponentInfo10Test.php +++ b/Test/Unit/ComponentInfo10Test.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Info component. - * - * @group yaml - * @group info */ +#[Group('yaml')] +#[Group('info')] class ComponentInfo10Test extends TestBase { /** diff --git a/Test/Unit/ComponentInfo9Test.php b/Test/Unit/ComponentInfo9Test.php index d47d6d41..41e80d51 100644 --- a/Test/Unit/ComponentInfo9Test.php +++ b/Test/Unit/ComponentInfo9Test.php @@ -2,16 +2,17 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; use Symfony\Component\Yaml\Yaml; /** * Tests for Info component. - * - * @group yaml - * @group info */ +#[Group('yaml')] +#[Group('info')] class ComponentInfo9Test extends TestBase { /** @@ -121,10 +122,9 @@ public static function dataExistingInfoFile() { /** * Tests with an existing info file. * - * @group existing - * - * @dataProvider dataExistingInfoFile */ + #[Group('existing')] + #[DataProvider('dataExistingInfoFile')] public function testExistingInfoFile($existing_dependencies, $generated_dependencies, $resulting_dependencies) { $module_name = 'test_module'; $module_data = [ diff --git a/Test/Unit/ComponentModule10Test.php b/Test/Unit/ComponentModule10Test.php index 0185c302..77943069 100644 --- a/Test/Unit/ComponentModule10Test.php +++ b/Test/Unit/ComponentModule10Test.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use MutableTypedData\Data\DataItem; /** * Tests basic module generation. - * - * @group hooks */ +#[Group('hooks')] class ComponentModule10Test extends TestBase { /** @@ -69,9 +69,8 @@ protected function simulateUiWalk(DataItem $data_item) { /** * Tests getting module configuration data. - * - * @group config */ + #[Group('config')] public function testConfiguration() { $config_data = \DrupalCodeBuilder\Factory::getTask('Configuration')->getConfigurationData('module'); $properties = $config_data->getProperties(); @@ -164,6 +163,7 @@ function testHelptextOption() { $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $module_file); $php_tester->assertDrupalCodingStandards(); + $this->assertStringContainsString('declare(strict_types=1);', $module_file); $php_tester->assertHasHookImplementation('hook_help', $module_name); $this->assertFunctionCode($module_file, $module_name . '_help', $help_text, "The hook_help() implementation contains the requested help text."); diff --git a/Test/Unit/ComponentModule7Test.php b/Test/Unit/ComponentModule7Test.php index a7a1cf1b..db52c5b0 100644 --- a/Test/Unit/ComponentModule7Test.php +++ b/Test/Unit/ComponentModule7Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests basic module generation. - * - * @group hooks */ +#[Group('hooks')] class ComponentModule7Test extends TestBase { /** diff --git a/Test/Unit/ComponentPHPFile10Test.php b/Test/Unit/ComponentPHPFile10Test.php index 47c43239..960e34ac 100644 --- a/Test/Unit/ComponentPHPFile10Test.php +++ b/Test/Unit/ComponentPHPFile10Test.php @@ -2,9 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use DrupalCodeBuilder\File\CodeFile; use DrupalCodeBuilder\Generator\PHPFile as RealPHPFile; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; +use MutableTypedData\DataItemFactory; +use MutableTypedData\Definition\DataDefinition; +use PHPUnit\Framework\Attributes\BeforeClass; use Prophecy\Argument; /** @@ -20,9 +24,31 @@ class ComponentPHPFile10Test extends TestBase { protected $drupalMajorVersion = 10; /** - * Test the qualified class name extraction. + * The dummy component data item. + * + * @var \MutableTypedData\Data\DataItem + */ + private static $mockedComponent; + + /** + * Sets up a mocked component data item. * - * @dataProvider providerQualifiedClassNameExtraction + * This is needed for self::testQualifiedClassNameExtraction(). + */ + #[BeforeClass] + public static function setUpMockedComponent() { + $definition = DataDefinition::create('complex') + ->setLabel('Component') + ->setProperties([ + 'root_component_name' => DataDefinition::create('string'), + ]); + + static::$mockedComponent = DataItemFactory::createFromDefinition($definition); + static::$mockedComponent->root_component_name = 'my_module'; + } + + /** + * Test the qualified class name extraction. * * @param string $code * The code to extract class names from. @@ -37,17 +63,21 @@ class ComponentPHPFile10Test extends TestBase { * extracted. * - NULL if we do not expect extraction. */ + #[DataProvider('providerQualifiedClassNameExtraction')] public function testQualifiedClassNameExtraction($code, $expected_changed_code, $expected_qualified_class_names) { // Make the protected method we're testing callable. $method = new \ReflectionMethod(PHPFile::class, 'extractFullyQualifiedClasses'); - $method->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $method->setAccessible(TRUE); + } - // Create a PHP file generator with some dummy constructor parameters. - $data_item = $this->prophesize(\MutableTypedData\Data\DataItem::class); - $php_file_generator = new PHPFile($data_item->reveal()); - // Our code is a single line, but the method expects an array of lines. - $code_lines = [$code]; + // Pass a component data item to PHPFile. Prophecy won't work as + // PHPFile::extractFullyQualifiedClasses() accesses a property, so we create + // one from a dummy defininition. + $php_file_generator = new PHPFile(static::$mockedComponent); + + $code_lines = explode("\n", $code); $imported_classes = []; @@ -57,13 +87,15 @@ public function testQualifiedClassNameExtraction($code, $expected_changed_code, $this->assertEmpty($imported_classes, "No class name was extracted."); } else { - if (!is_array($expected_qualified_class_names)) { - $expected_qualified_class_names = [$expected_qualified_class_names]; + if (is_array($expected_qualified_class_names)) { + $this->assertEquals($expected_qualified_class_names, $imported_classes, "The qualified class name was extracted."); + } + else { + $this->assertCount(1, $imported_classes, "The qualified class name was extracted."); + $this->assertArrayHasKey($expected_qualified_class_names, $imported_classes, "The qualified class name was extracted."); } - $this->assertEquals($expected_qualified_class_names, $imported_classes, "The qualified class name was extracted."); - $changed_code = array_pop($code_lines); - $this->assertEquals($expected_changed_code, $changed_code, "The code was changed to use the short class name."); + $this->assertEquals(explode("\n", $expected_changed_code), $code_lines, "The code was changed to use the short class name."); } } @@ -95,8 +127,8 @@ public static function providerQualifiedClassNameExtraction() { 'function myfunc(\Foo\Bar $param_1, \Bar\Bax\Biz $param_2, \BuiltIn $param_3) {', 'function myfunc(Bar $param_1, Biz $param_2, \BuiltIn $param_3) {', [ - 'Foo\Bar', - 'Bar\Bax\Biz', + 'Foo\Bar' => NULL, + 'Bar\Bax\Biz' => NULL, ] ], 'static call' => [ @@ -115,12 +147,32 @@ public static function providerQualifiedClassNameExtraction() { 'Foo\Bar', ], 'repeated' => [ - '$foo = new \Foo\Bar(); - $bar = new \Foo\Bar();', - '$foo = new Bar(); - $bar = new Bar();', + <<<'EOT' + call(new \Foo\Bar(), new \Foo\Bar()); + $bar = new \Foo\Bar(); + EOT, + <<<'EOT' + call(new Bar(), new Bar()); + $bar = new Bar(); + EOT, 'Foo\Bar', ], + 'clash-vendor' => [ + '\Other\Foo\Bar::class; \Drupal\foo\Bar::class', + 'OtherBar::class; Bar::class', + [ + 'Drupal\foo\Bar' => NULL, + 'Other\Foo\Bar' => 'OtherBar', + ] + ], + 'clash-module' => [ + '\Drupal\my_module\Foo\Bar::class; \Drupal\my_module\Biz\Bar::class', + 'FooBar::class; BizBar::class', + [ + 'Drupal\my_module\Foo\Bar' => 'FooBar', + 'Drupal\my_module\Biz\Bar' => 'BizBar', + ] + ], 'current' => [ '$foo = new \Current\Namespace\Bar()', '$foo = new Bar()', @@ -205,6 +257,9 @@ public function testClassImportsOrder() { EOT, ], ]); + $report_hook_data->getVariantMapping(Argument::any())->willReturn([ + 'hook_cake' => 'literal', + ]); $report_hook_data->getOptions(Argument::any())->willReturn([ 'hook_cake' => 'hook_cake()', ]); diff --git a/Test/Unit/ComponentPermissions10Test.php b/Test/Unit/ComponentPermissions10Test.php index dddc9acb..9f4c319c 100644 --- a/Test/Unit/ComponentPermissions10Test.php +++ b/Test/Unit/ComponentPermissions10Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests the Permissions generator class. - * - * @group yaml */ +#[Group('yaml')] class ComponentPermissions10Test extends TestBase { /** diff --git a/Test/Unit/ComponentPermissions7Test.php b/Test/Unit/ComponentPermissions7Test.php index 7571ff06..42507ba8 100644 --- a/Test/Unit/ComponentPermissions7Test.php +++ b/Test/Unit/ComponentPermissions7Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests the Permissions generator class. - * - * @group hooks */ +#[Group('hooks')] class ComponentPermissions7Test extends TestBase { /** diff --git a/Test/Unit/ComponentPluginType10Test.php b/Test/Unit/ComponentPluginType10Test.php index f0a44805..0a428fea 100644 --- a/Test/Unit/ComponentPluginType10Test.php +++ b/Test/Unit/ComponentPluginType10Test.php @@ -2,15 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** - * Tests the Plugin Type generator class. - * - * @group yaml - * @group di + * Tests generation of plugin types. */ +#[Group('yaml')] +#[Group('di')] class ComponentPluginType10Test extends TestBase { /** @@ -21,7 +21,7 @@ class ComponentPluginType10Test extends TestBase { protected $drupalMajorVersion = 10; /** - * Test Plugin Type component for attribute plugins. + * Test a plugin type with attribute plugins. */ function testAttributePluginTypeBasic() { // Create a module. @@ -156,10 +156,77 @@ function testAttributePluginTypeBasic() { 'module_handler' => 'Drupal\Core\Extension\ModuleHandlerInterface', ]); $constructor_tester->assertPromotedParameters([], 'No plugin manager parameters are promoted.'); + + // Test with values instead of defaults. + $module_data = [ + 'base' => 'module', + 'root_name' => 'test_module', + 'readable_name' => 'Test module', + 'short_description' => 'Test Module description', + 'hooks' => [ + ], + 'plugin_types' => [ + 0 => [ + 'discovery_type' => 'attribute', + 'plugin_type' => 'cat_feeder', + 'attribute_class' => 'Caaaaat', + 'attribute_properties' => [ + 0 => [ + 'name' => 'fishiness', + 'type' => 'int', + 'description' => 'How fishy is it?', + ], + ], + ] + ], + 'readme' => FALSE, + ]; + $files = $this->generateModuleFiles($module_data); + + $this->assertArrayHasKey('src/Attribute/Caaaaat.php', $files); + + $plugin_manager_file = $files["src/CatFeederManager.php"]; + // We know the syntax is fine from the earlier assertion, so just check the + // right class is there. + $this->assertStringContainsString('Caaaaat::class', $plugin_manager_file); } /** - * Test Plugin Type component. + * Tests a plugin type with attributes and BC handling for annotations. + * + * This relies on the + * \Drupal\test_module_plugin_type_with_bc\Annotation\Unique fixture class at + * the end of this file. + */ + function testAttributePluginTypeBCHandling() { + $module_data = [ + 'base' => 'module', + // Use unique names so the fixture doesn't clash. + 'root_name' => 'test_module_plugin_type_with_bc', + 'readable_name' => 'Test module', + 'short_description' => 'Test Module description', + 'hooks' => [], + 'plugin_types' => [ + 0 => [ + 'discovery_type' => 'attribute', + 'plugin_type' => 'unique', + ], + ], + 'readme' => FALSE, + ]; + $files = $this->generateModuleFiles($module_data); + + $plugin_manager_file = $files["src/UniqueManager.php"]; + + $this->assertStringContainsString('use Drupal\test_module_plugin_type_with_bc\Attribute\Unique as AttributeUnique;', $plugin_manager_file); + $this->assertStringContainsString('use Drupal\test_module_plugin_type_with_bc\Annotation\Unique as AnnotationUnique;', $plugin_manager_file); + + $this->assertStringContainsString('AttributeUnique::class', $plugin_manager_file); + $this->assertStringContainsString('AnnotationUnique::class', $plugin_manager_file); + } + + /** + * Test a plugin type for annotation plugins. */ function testAnnotationPluginTypeBasic() { // Create a module. @@ -519,9 +586,8 @@ function testMultiplePluginTypes() { /** * Test Plugin Type with DI in the plugin base class. - * - * @group di */ + #[Group('di')] function testAttributePluginTypeWithServices() { $module_data = [ 'base' => 'module', @@ -585,3 +651,9 @@ function testAttributePluginTypeWithServices() { } } + +/** + * Fixture class for testAttributePluginTypeBCHandling(). + */ +namespace Drupal\test_module_plugin_type_with_bc\Annotation; +class Unique {} diff --git a/Test/Unit/ComponentPluginsAnnotated9Test.php b/Test/Unit/ComponentPluginsAnnotated9Test.php index 6bebaafd..8f1f4394 100644 --- a/Test/Unit/ComponentPluginsAnnotated9Test.php +++ b/Test/Unit/ComponentPluginsAnnotated9Test.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; use MutableTypedData\Exception\InvalidInputException; @@ -15,10 +17,9 @@ * types left. * * TODO: Restore this to Drupal 10 if I CBA. - * - * @group yaml - * @group plugin */ +#[Group('yaml')] +#[Group('plugin')] class ComponentPluginsAnnotated9Test extends TestBase { /** @@ -128,9 +129,8 @@ function testPluginsGenerationClassName() { * Tests special cases where prefixing of the plugin name should be skipped. * * This also tests that derivative plugin IDs are handled correctly. - * - * @dataProvider providerPluginsGenerationNamePrefixing */ + #[DataProvider('providerPluginsGenerationNamePrefixing')] public function testPluginsGenerationNamePrefixing(string $plugin_id, string $filename) { // Create a module. $module_name = 'test_module'; @@ -213,7 +213,7 @@ function testPluginsGenerationDeriver() { $deriver = $files['src/Plugin/Derivative/AlphaBlockDeriver.php']; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $deriver); $php_tester->assertClassHasParent('Drupal\Component\Plugin\Derivative\DeriverBase'); - $php_tester->assertClassHasInterfaces(['Drupal\Core\Plugin\Discovery\ContainerDeriverInterface']); + $php_tester->assertClassHasInterfaces(['Drupal\Component\Plugin\Derivative\DeriverInterface']); $php_tester->assertHasMethod('getDerivativeDefinitions'); // Check the plugin file declares the deriver. @@ -405,6 +405,7 @@ function testPluginWithOnlyId() { 'short_description' => 'Test Module description', 'hooks' => [ ], + 'hook_implementation_type' => 'procedural', 'plugins' => [ 0 => [ 'plugin_type' => 'element_info', @@ -495,9 +496,8 @@ function testPluginsGenerationBadPluginType() { /** * Test Plugins component with injected services. - * - * @group di */ + #[Group('di')] function testPluginsGenerationWithServices() { // Create a module. $module_name = 'test_module'; @@ -572,9 +572,8 @@ function testPluginsGenerationWithServices() { /** * Test Plugins component with a plugin base class with an existing create(). - * - * @group di */ + #[Group('di')] function testPluginsGenerationWithExistingCreate() { // Create a module. $module_name = 'test_module'; @@ -657,9 +656,8 @@ function testPluginsGenerationWithExistingCreate() { /** * Tests a plugin base class with nonstandard fixed constructor parameters. - * - * @group di */ + #[Group('di')] function testPluginsGenerationWithNonstandardFixedParameters() { // Create a module. $module_name = 'test_module'; @@ -847,6 +845,7 @@ function testRenderElement() { 'root_name' => $module_name, 'readable_name' => 'Test module', 'short_description' => 'Test Module description', + 'hook_implementation_type' => 'procedural', 'plugins' => [ 0 => [ 'plugin_type' => 'element_info', diff --git a/Test/Unit/ComponentPluginsAttribute11Test.php b/Test/Unit/ComponentPluginsAttribute11Test.php index e57dbb9c..e48b586a 100644 --- a/Test/Unit/ComponentPluginsAttribute11Test.php +++ b/Test/Unit/ComponentPluginsAttribute11Test.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; use MutableTypedData\Exception\InvalidInputException; @@ -12,10 +14,9 @@ * Tests generation of attribute plugins. * * TODO: This is missing testing of the actual attributes! - * - * @group yaml - * @group plugin */ +#[Group('yaml')] +#[Group('plugin')] class ComponentPluginsAttribute11Test extends TestBase { /** @@ -206,9 +207,8 @@ function testPluginsGenerationClassName() { * Tests special cases where prefixing of the plugin name should be skipped. * * This also tests that derivative plugin IDs are handled correctly. - * - * @dataProvider providerPluginsGenerationNamePrefixing */ + #[DataProvider('providerPluginsGenerationNamePrefixing')] public function testPluginsGenerationNamePrefixing(string $plugin_id, string $filename) { // Create a module. $module_name = 'test_module'; @@ -265,6 +265,7 @@ public static function providerPluginsGenerationNamePrefixing() { /** * Tests plugin derivers. */ + #[Group('di')] function testPluginsGenerationDeriver() { // Create a module. $module_name = 'test_module'; @@ -277,7 +278,9 @@ function testPluginsGenerationDeriver() { ], 'plugins' => [ 0 => [ - 'plugin_type' => 'block', + // Use a plugin type whose name has a '.' in to test the formation of + // the deriver class name. + 'plugin_type' => 'field.formatter', 'plugin_name' => 'alpha', 'deriver' => TRUE, ] @@ -286,19 +289,49 @@ function testPluginsGenerationDeriver() { ]; $files = $this->generateModuleFiles($module_data); - $this->assertArrayHasKey('src/Plugin/Derivative/AlphaBlockDeriver.php', $files); + $this->assertArrayHasKey('src/Plugin/Derivative/AlphaFieldFormatterDeriver.php', $files); - $deriver = $files['src/Plugin/Derivative/AlphaBlockDeriver.php']; + $deriver = $files['src/Plugin/Derivative/AlphaFieldFormatterDeriver.php']; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $deriver); $php_tester->assertClassHasParent('Drupal\Component\Plugin\Derivative\DeriverBase'); - $php_tester->assertClassHasInterfaces(['Drupal\Core\Plugin\Discovery\ContainerDeriverInterface']); + $php_tester->assertClassHasInterfaces(['Drupal\Component\Plugin\Derivative\DeriverInterface']); $php_tester->assertHasMethod('getDerivativeDefinitions'); // Check the plugin file declares the deriver. - $plugin_file = $files["src/Plugin/Block/Alpha.php"]; + $plugin_file = $files["src/Plugin/Field/FieldFormatter/Alpha.php"]; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $plugin_file); - $php_tester->assertImportsClassLike(['Drupal\test_module\Plugin\Derivative\AlphaBlockDeriver']); - $php_tester->assertClassAttributeHasNamedParameterValue('deriver', 'AlphaBlockDeriver::class', 'Block'); + $php_tester->assertImportsClassLike(['Drupal\test_module\Plugin\Derivative\AlphaFieldFormatterDeriver']); + $php_tester->assertClassAttributeHasNamedParameterValue('deriver', 'AlphaFieldFormatterDeriver::class', 'FieldFormatter'); + + // Add DI to the deriver class. + $module_data['plugins'][0]['deriver_injected_services'] = [ + 'current_user', + 'entity_type.manager', + ]; + + $files = $this->generateModuleFiles($module_data); + + $this->assertArrayHasKey('src/Plugin/Derivative/AlphaFieldFormatterDeriver.php', $files); + $deriver = $files['src/Plugin/Derivative/AlphaFieldFormatterDeriver.php']; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $deriver); + $php_tester->assertClassHasParent('Drupal\Component\Plugin\Derivative\DeriverBase'); + $php_tester->assertClassHasInterfaces(['Drupal\Core\Plugin\Discovery\ContainerDeriverInterface']); + // Check service injection. + $php_tester->assertInjectedServicesWithFactory([ + [ + 'typehint' => 'Drupal\Core\Session\AccountProxyInterface', + 'service_name' => 'current_user', + 'property_name' => 'currentUser', + 'parameter_name' => 'current_user', + ], + [ + 'typehint' => 'Drupal\Core\Entity\EntityTypeManagerInterface', + 'service_name' => 'entity_type.manager', + 'property_name' => 'entityTypeManager', + 'parameter_name' => 'entity_type_manager', + ], + ]); } /** @@ -431,9 +464,8 @@ function testPluginsGenerationBadPluginType() { /** * Test Plugins component with injected services. - * - * @group di */ + #[Group('di')] function testPluginsGenerationWithServices() { // Create a module. $module_name = 'test_module'; @@ -562,6 +594,69 @@ function testPluginsGenerationWithOtherSchema() { // TODO: assert deeper into the YAML. } + /** + * Test the validation constraint plugin variant. + */ + #[Group('di')] + public function testPluginValidationConstraint(): void { + // Create a module. + $module_name = 'test_module'; + $module_data = [ + 'base' => 'module', + 'root_name' => $module_name, + 'readable_name' => 'Test module', + 'short_description' => 'Test Module description', + 'plugins' => [ + 0 => [ + 'plugin_type' => 'validation.constraint', + 'plugin_name' => 'alpha', + 'injected_services' => [ + 'entity_type.manager', + ], + ], + ], + 'readme' => FALSE, + ]; + $files = $this->generateModuleFiles($module_data); + + $this->assertFiles([ + "$module_name.info.yml", + "src/Plugin/Validation/Constraint/Alpha.php", + "src/Plugin/Validation/Constraint/AlphaValidator.php", + ], $files); + + $plugin = $files['src/Plugin/Validation/Constraint/Alpha.php']; + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $plugin); + $php_tester->assertDrupalCodingStandards(); + $php_tester->assertHasClass('Drupal\test_module\Plugin\Validation\Constraint\Alpha'); + // TODO: Quick hack because class tests don't support import aliases. + // $php_tester->assertClassHasParent('Symfony\Component\Validator\Constraint'); + $this->assertStringContainsString('use Symfony\Component\Validator\Constraint as SymfonyConstraint;', $plugin); + $this->assertStringContainsString('extends SymfonyConstraint', $plugin); + + $validator = $files["src/Plugin/Validation/Constraint/AlphaValidator.php"]; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $validator); + $php_tester->assertDrupalCodingStandards(); + $php_tester->assertHasClass('Drupal\test_module\Plugin\Validation\Constraint\AlphaValidator'); + $php_tester->assertClassHasParent('Symfony\Component\Validator\ConstraintValidator'); + $php_tester->assertHasMethod('validate'); + + // Check service injection. + $php_tester->assertClassHasInterfaces([ + 'Drupal\Core\DependencyInjection\ContainerInjectionInterface', + ]); + $php_tester->assertInjectedServicesWithFactory([ + [ + 'typehint' => 'Drupal\Core\Entity\EntityTypeManagerInterface', + 'service_name' => 'entity_type.manager', + 'property_name' => 'entityTypeManager', + 'parameter_name' => 'entity_type_manager', + ], + ]); + + } + } namespace Drupal\Component\Plugin\Exception; diff --git a/Test/Unit/ComponentPluginsYAML10Test.php b/Test/Unit/ComponentPluginsYAML10Test.php index 041689a9..4e977181 100644 --- a/Test/Unit/ComponentPluginsYAML10Test.php +++ b/Test/Unit/ComponentPluginsYAML10Test.php @@ -2,15 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests the PluginYAMLDiscovery generator class. - * - * @group yaml - * @group plugin */ +#[Group('yaml')] +#[Group('plugin')] class ComponentPluginsYAML10Test extends TestBase { /** @@ -91,7 +91,7 @@ function testYamlPluginsGenerationDeriver() { $deriver = $files['src/Plugin/Derivative/AlphaMenuLinkDeriver.php']; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $deriver); $php_tester->assertClassHasParent('Drupal\Component\Plugin\Derivative\DeriverBase'); - $php_tester->assertClassHasInterfaces(['Drupal\Core\Plugin\Discovery\ContainerDeriverInterface']); + $php_tester->assertClassHasInterfaces(['Drupal\Component\Plugin\Derivative\DeriverInterface']); $php_tester->assertHasMethod('getDerivativeDefinitions'); // Check the plugin YAML file declares the deriver. @@ -102,6 +102,71 @@ function testYamlPluginsGenerationDeriver() { $yaml_tester->assertPropertyHasValue(['test_module.alpha', 'deriver'], '\Drupal\test_module\Plugin\Derivative\AlphaMenuLinkDeriver'); } + /** + * Tests a custom plugin class with DI. + */ + #[Group('di')] + public function testCustomPluginClass(): void { + // Create a module. + $module_data = [ + 'base' => 'module', + 'root_name' => 'test_module', + 'readable_name' => 'Test module', + 'short_description' => 'Test Module description', + 'hooks' => [ + ], + 'plugins' => [ + 0 => [ + 'plugin_type' => 'menu.link', + 'plugin_name' => 'alpha', + 'plugin_custom_class' => TRUE, + 'injected_services' => [ + 'current_user', + ], + ] + ], + 'readme' => FALSE, + ]; + $files = $this->generateModuleFiles($module_data); + + $this->assertFiles([ + 'test_module.info.yml', + 'test_module.links.menu.yml', + 'src/Plugin/Menu/Link/Alpha.php', + ], $files); + + $plugins_file = $files['test_module.links.menu.yml']; + + $yaml_tester = new YamlTester($plugins_file); + $yaml_tester->assertHasProperty('test_module.alpha'); + $yaml_tester->assertPropertyHasValue(['test_module.alpha', 'class'], '\Drupal\test_module\Plugin\Menu\Link\Alpha'); + + $plugin_file = $files['src/Plugin/Menu/Link/Alpha.php']; + + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $plugin_file); + $php_tester->assertDrupalCodingStandards(); + $php_tester->assertHasClass('Drupal\test_module\Plugin\Menu\Link\Alpha'); + $php_tester->assertClassHasParent('Drupal\Core\Menu\MenuLinkBase'); + + // Check service injection. + $php_tester->assertClassHasInterfaces([ + 'Drupal\Core\Plugin\ContainerFactoryPluginInterface', + ]); + $php_tester->assertInjectedServicesWithFactory([ + [ + 'typehint' => 'Drupal\Core\Session\AccountProxyInterface', + 'service_name' => 'current_user', + 'property_name' => 'currentUser', + 'parameter_name' => 'current_user', + ], + ]); + $php_tester->assertConstructorBaseParameters([ + 'configuration' => 'array', + 'plugin_id' => NULL, + 'plugin_definition' => NULL, + ]); + } + /** * Test PluginYAML component with annotated plugins too. */ diff --git a/Test/Unit/ComponentProfile10Test.php b/Test/Unit/ComponentProfile10Test.php index 14923f87..7c37fcd5 100644 --- a/Test/Unit/ComponentProfile10Test.php +++ b/Test/Unit/ComponentProfile10Test.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; use MutableTypedData\Data\DataItem; @@ -67,9 +68,8 @@ protected function simulateUiWalk(DataItem $data_item) { /** * Tests getting module configuration data. - * - * @group config */ + #[Group('config')] public function testConfiguration() { $config_data = \DrupalCodeBuilder\Factory::getTask('Configuration')->getConfigurationData('profile'); $properties = $config_data->getProperties(); diff --git a/Test/Unit/ComponentReadme10Test.php b/Test/Unit/ComponentReadme10Test.php index 62183bc6..77270648 100644 --- a/Test/Unit/ComponentReadme10Test.php +++ b/Test/Unit/ComponentReadme10Test.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; /** * Tests the README file. @@ -143,8 +144,6 @@ public static function dataReadmeWithDependencies() { /** * Tests the README file with dependencies. * - * @dataProvider dataReadmeWithDependencies - * * @param boolean $readme * The value for the readme property in the module data. * @param array $dependencies @@ -156,6 +155,7 @@ public static function dataReadmeWithDependencies() { * @param boolean $expect_requirements_dependencies * Whether to expect a list of dependencies in the Requirements section. */ + #[DataProvider('dataReadmeWithDependencies')] function testReadmeWithDependencies(bool $readme, array $dependencies, bool $expect_readme, bool $expect_requirements_section, bool $expect_requirements_dependencies) { // Create a module. $module_data = [ diff --git a/Test/Unit/ComponentRouteCallback9Test.php b/Test/Unit/ComponentRouteCallback9Test.php deleted file mode 100644 index e47e59c0..00000000 --- a/Test/Unit/ComponentRouteCallback9Test.php +++ /dev/null @@ -1,65 +0,0 @@ - 'module', - 'root_name' => 'test_module', - 'readable_name' => 'Test Module', - 'dynamic_routes' => [ - 0 => [ - 'provider_class_short_name' => 'MyRouteProvider', - ], - 1 => [ - 'provider_class_short_name' => 'OtherRouteProvider', - ], - ], - 'readme' => FALSE, - ]; - - $files = $this->generateModuleFiles($module_data); - - $this->assertFiles([ - 'test_module.info.yml', - 'test_module.routing.yml', - 'src/Routing/MyRouteProvider.php', - 'src/Routing/OtherRouteProvider.php', - ], $files); - - $routing_file = $files['test_module.routing.yml']; - $yaml_tester = new YamlTester($routing_file); - - $yaml_tester->assertHasProperty('route_callbacks', "The routing file has the callbacks property."); - $yaml_tester->assertPropertyHasValue(['route_callbacks', 0], '\Drupal\test_module\Routing\MyRouteProvider::routes', "The routing file declares the route path."); - $yaml_tester->assertPropertyHasValue(['route_callbacks', 1], '\Drupal\test_module\Routing\OtherRouteProvider::routes', "The routing file declares the route path."); - - $provider_file = $files['src/Routing/MyRouteProvider.php']; - - $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $provider_file); - $php_tester->assertDrupalCodingStandards(); - $php_tester->assertHasClass('Drupal\test_module\Routing\MyRouteProvider'); - - $method_tester = $php_tester->getMethodTester('routes'); - $method_tester->getDocBlockTester()->assertHasLine('Returns an array of routes.'); - } - -} diff --git a/Test/Unit/ComponentRouterItem10Test.php b/Test/Unit/ComponentRouterItem10Test.php index bfa8afba..f6e11cfb 100644 --- a/Test/Unit/ComponentRouterItem10Test.php +++ b/Test/Unit/ComponentRouterItem10Test.php @@ -2,14 +2,15 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Router item component. - * - * @group yaml */ +#[Group('yaml')] class ComponentRouterItem10Test extends TestBase { /** @@ -227,9 +228,8 @@ public function testRouteControllerTypes() { * @param array $class_names_and_methods * (optional) An array of data about expected classes and their methods. * Keys are the relative class names. Values are arrays of method names. - * - * @dataProvider dataRouteAccessTypes */ + #[DataProvider('dataRouteAccessTypes')] public function testRouteAccessTypes( array $access, string $yaml_property, @@ -345,12 +345,9 @@ public static function dataRouteAccessTypes() { ], '_custom_access', '\Drupal\test_module\Controller\MyPathControllerController::access', - [ - 'controller_type' => 'controller', - ], + [], [ 'Controller\MyPathControllerController' => [ - 'content', 'access', ], ], @@ -365,9 +362,12 @@ public static function dataRouteAccessTypes() { ], '_custom_access', '\Drupal\test_module\Controller\MyPathControllerController::access', - [], + [ + 'controller_type' => 'controller', + ], [ 'Controller\MyPathControllerController' => [ + 'content', 'access', ], ], @@ -514,9 +514,8 @@ public function testRouteControllerClass() { /** * Test options for the controller class - * - * @group di */ + #[Group('di')] public function testRouteControllerClassWithDI() { // Assemble module data. $module_name = 'test_module'; @@ -634,9 +633,8 @@ public function testRouteForm() { /** * Test generating a route with a menu link. - * - * @group plugin */ + #[Group('plugin')] public function testRouteGenerationWithMenuLink() { // Assemble module data. $module_name = 'test_module'; @@ -692,9 +690,8 @@ public function testRouteGenerationWithMenuLink() { /** * Test generating a route with a menu tab. - * - * @group plugin */ + #[Group('plugin')] public function testRouteGenerationWithMenuTab() { // Assemble module data. $module_name = 'test_module'; @@ -742,9 +739,8 @@ public function testRouteGenerationWithMenuTab() { /** * Tests adoption of existing form. - * - * @group adopt */ + #[Group('adopt')] public function testExistingRouterItemAdoption() { // First pass: generate the files we'll mock as existing. $module_name = 'existing'; diff --git a/Test/Unit/ComponentRouterItem7Test.php b/Test/Unit/ComponentRouterItem7Test.php index 388ff874..7b8a8b73 100644 --- a/Test/Unit/ComponentRouterItem7Test.php +++ b/Test/Unit/ComponentRouterItem7Test.php @@ -2,13 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests for Router item component. - * - * @group hooks */ +#[Group('hooks')] class ComponentRouterItem7Test extends TestBase { /** diff --git a/Test/Unit/ComponentService10Test.php b/Test/Unit/ComponentService11Test.php similarity index 96% rename from Test/Unit/ComponentService10Test.php rename to Test/Unit/ComponentService11Test.php index 975ddd04..6015bfd0 100644 --- a/Test/Unit/ComponentService10Test.php +++ b/Test/Unit/ComponentService11Test.php @@ -2,23 +2,24 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for Service component. - * - * @group yaml */ -class ComponentService10Test extends TestBase { +#[Group('yaml')] +class ComponentService11Test extends TestBase { /** * The Drupal core major version to set up for this test. * * @var int */ - protected $drupalMajorVersion = 10; + protected $drupalMajorVersion = 11; /** * Test generating a module with a service. @@ -64,14 +65,14 @@ public function testBasicServiceGeneration() { $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $service_class_file); $php_tester->assertDrupalCodingStandards(); + $this->assertStringContainsString('declare(strict_types=1);', $service_class_file); $php_tester->assertHasClass('Drupal\test_module\MyService'); } /** * Test namespace configuration for service generation. - * - * @group config */ + #[Group('config')] public function testServiceGenerationNamespaceConfiguration() { // Assemble module data. $module_name = 'test_module'; @@ -151,9 +152,8 @@ public function testServiceGenerationNamespaceConfiguration() { /** * Test YAML linebreaks configuration for service generation. - * - * @group config */ + #[Group('config')] public function testServiceGenerationYamlLinebreaksConfiguration() { $module_data = [ 'base' => 'module', @@ -191,9 +191,8 @@ public function testServiceGenerationYamlLinebreaksConfiguration() { /** * Test service parameter expansion configuration. - * - * @group config */ + #[Group('config')] public function testServiceGenerationYamlParameterLinebreaksConfiguration() { $module_data = [ 'base' => 'module', @@ -235,9 +234,8 @@ public function testServiceGenerationYamlParameterLinebreaksConfiguration() { /** * Test generating a module with a service using a preset. - * - * @group presets */ + #[Group('presets')] public function testServiceGenerationFromPreset() { // Assemble module data. $module_name = 'test_module'; @@ -365,6 +363,26 @@ public static function providerServiceGenerationWithServices() { ], ], ], + // Class name service. + 'class-named' => [ + 'injected_services' => [ + 'Drupal\Core\DefaultContent\Importer', + ], + 'property_promotion' => FALSE, + 'yaml_arguments' => [ + '@Drupal\Core\DefaultContent\Importer', + ], + 'assert_injected_services' => [ + [ + // Eh this typehint is a whole other problem with this particular + // service. + 'typehint' => 'Psr\Log\LoggerAwareInterface', + 'service_name' => 'importer', + 'property_name' => 'importer', + 'parameter_name' => 'importer', + ], + ], + ], // Pseudoservice with the real service also present as a parameter. 'pseudo-with-real' => [ 'injected_services' => [ @@ -463,10 +481,9 @@ public static function providerServiceGenerationWithServices() { /** * Test a service with with injected services. * - * @group di - * - * @dataProvider providerServiceGenerationWithServices */ + #[Group('di')] + #[DataProvider('providerServiceGenerationWithServices')] function testServiceGenerationWithServices($injected_services, bool $property_promotion, $yaml_arguments, $assert_injected_services) { // Assemble module data. $module_name = 'test_module'; @@ -510,6 +527,7 @@ function testServiceGenerationWithServices($injected_services, bool $property_pr $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $service_class_file); $php_tester->assertDrupalCodingStandards(); $php_tester->assertHasClass('Drupal\test_module\MyService'); + $php_tester->assertNotClassHasInterfaces(['Drupal\Core\DependencyInjection\ContainerInjectionInterface']); // Check service injection. $php_tester->assertInjectedServices($assert_injected_services, $property_promotion); @@ -517,9 +535,8 @@ function testServiceGenerationWithServices($injected_services, bool $property_pr /** * Test several services injecting the same services. - * - * @group di */ + #[Group('di')] function testServiceGenerationRepeatedInjectedServies() { $module_name = 'test_module'; $module_data = [ @@ -805,9 +822,8 @@ class: Drupal\existing\Alpha /** * Test a service that decorates another. - * - * @group di */ + #[Group('di')] function testServiceGenerationWithDecoration() { // Assemble module data: decoration without any injected services. $module_data = [ @@ -919,11 +935,10 @@ function testServiceGenerationWithDecoration() { * The expected YAML defining the list of existing services, without the * initial 'services' key. NULL if no file is expected to be generated. * - * @group existing - * - * @dataProvider dataExistingServicesYamlFile * */ + #[Group('existing')] + #[DataProvider('dataExistingServicesYamlFile')] public function testExistingServicesYamlFile(?string $existing, ?array $generated, ?string $resulting) { $module_name = 'existing'; $module_data = [ @@ -1057,9 +1072,6 @@ public static function dataExistingServiceMerge() { /** * Test merging of injected services in an existing service. * - * @group existing - * - * @dataProvider dataExistingServiceMerge * * @param array $existing_service_data * The module data for a single service to generate as the mocked existing @@ -1070,6 +1082,8 @@ public static function dataExistingServiceMerge() { * The expected injected services, in the format expected by * assertInjectedServices(). */ + #[Group('existing')] + #[DataProvider('dataExistingServiceMerge')] public function testExistingServiceMerge($existing_service_data, $generated_service_data, $expected_injected_services) { // First pass: generate the files we'll mock as existing. $module_name = 'existing'; @@ -1211,10 +1225,9 @@ public static function dataExistingServicesAdoption() { * @param string $adopted_relative_class * The relative class name of the adopted service. * - * @group adopt - * - * @dataProvider dataExistingServicesAdoption */ + #[Group('adopt')] + #[DataProvider('dataExistingServicesAdoption')] public function testExistingServicesAdoption(array $data_services, string $adopted_relative_class) { // Mock an existing module. $extension = new MockableExtension('module', __DIR__ . '/../Fixtures/modules/existing/'); diff --git a/Test/Unit/ComponentServiceEventSubscriber10Test.php b/Test/Unit/ComponentServiceEventSubscriber10Test.php index 130560b1..b8efd469 100644 --- a/Test/Unit/ComponentServiceEventSubscriber10Test.php +++ b/Test/Unit/ComponentServiceEventSubscriber10Test.php @@ -2,14 +2,14 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; /** * Tests for event subscriber component. - * - * @group yaml */ +#[Group('yaml')] class ComponentServiceEventSubscriber10Test extends TestBase { /** diff --git a/Test/Unit/ComponentTests8Test.php b/Test/Unit/ComponentTests8Test.php index f1bf2fc5..f360d69b 100644 --- a/Test/Unit/ComponentTests8Test.php +++ b/Test/Unit/ComponentTests8Test.php @@ -65,7 +65,11 @@ function testModuleGenerationTests() { $tests_file = $files['src/Tests/TestModuleTestCase.php']; $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $tests_file); - $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); + + $excluded = $this->phpcsExcludedSniffs; + $excluded[] = 'Generic.Arrays.DisallowLongArraySyntax'; + + $php_tester->assertDrupalCodingStandards($excluded); $php_tester->assertHasClass('TestModuleTestCase', "The test class file contains the correct class"); $php_tester->assertHasMethods(['getInfo', 'setUp', 'testTodoChangeThisName']); } diff --git a/Test/Unit/ComponentTestsPHPUnit10Test.php b/Test/Unit/ComponentTestsPHPUnit10Test.php index 51b5bd5d..7194dbdb 100644 --- a/Test/Unit/ComponentTestsPHPUnit10Test.php +++ b/Test/Unit/ComponentTestsPHPUnit10Test.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; @@ -69,7 +71,7 @@ function testModuleGenerationTestsWithModuleDependencies() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\Kernel\MyTest'); $php_tester->assertHasMethods(['setUp', 'testMyTest']); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'dependency_one', @@ -150,7 +152,7 @@ function testModuleGenerationKernelTest() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\Kernel\MyTest'); $php_tester->assertClassHasParent('Drupal\KernelTests\KernelTestBase'); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'test_module', @@ -193,7 +195,7 @@ function testBrowserTest() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\Functional\MyTest'); $php_tester->assertClassHasParent('Drupal\Tests\BrowserTestBase'); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'test_module', @@ -237,7 +239,7 @@ function testJavaScriptTest() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\FunctionalJavascript\MyTest'); $php_tester->assertClassHasParent('Drupal\FunctionalJavascriptTests\WebDriverTestBase'); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'test_module', @@ -271,10 +273,9 @@ public static function dataModuleGenerationTestsWithServices(): array { /** * Create a test class with services. * - * @group di - * - * @dataProvider dataModuleGenerationTestsWithServices */ + #[Group('di')] + #[DataProvider('dataModuleGenerationTestsWithServices')] function testModuleGenerationTestsWithServices(bool $container_services = FALSE, bool $mocked_services = FALSE) { // Create a module. $module_name = 'test_module'; @@ -340,9 +341,8 @@ function testModuleGenerationTestsWithServices(bool $container_services = FALSE, /** * Create a test class with a test module. - * - * @group test */ + #[Group('test')] function testModuleGenerationTestsWithBasicTestModule() { // Create a module. $module_name = 'generated_module'; @@ -375,9 +375,8 @@ function testModuleGenerationTestsWithBasicTestModule() { /** * Create a test class with a test module and module components. - * - * @group test */ + #[Group('test')] function testModuleGenerationTestsWithTestModuleComponents() { // Create a module. $module_name = 'generated_module'; @@ -518,7 +517,7 @@ function testModuleGenerationTestsWithTestModuleComponents() { 'generated_module', 'my_test', ]; - $php_tester->assertClassHasProtectedProperty('modules', 'array', $expected_modules_property_value); + $php_tester->assertClassHasProtectedProperty('modules', NULL, $expected_modules_property_value); } /** @@ -554,11 +553,6 @@ public static function dataTestModuleWithExistingFunctions() { * This generates a hook in either the main or the test module, with an * existing function is either the main or test module file. * - * @group existing - * @group test - * - * @dataProvider dataTestModuleWithExistingFunctions - * * @param string $generated * Where the generated hook goes. One of: * - 'main': The main generated module. @@ -568,6 +562,9 @@ public static function dataTestModuleWithExistingFunctions() { * - 'main': The main generated module. * - 'test': The test module. */ + #[Group('existing')] + #[Group('test')] + #[DataProvider('dataTestModuleWithExistingFunctions')] public function testTestModuleWithExistingFunctions(string $generated, string $existing_code) { // Create a module. $module_data = [ @@ -680,11 +677,6 @@ function $existing_function_name() { * This generates a service in either the main or the test module, with an * existing service is either the main or test module. * - * @group existing - * @group test - * - * @dataProvider dataTestModuleWithExistingFunctions - * * @param string $generated * Where the generated service goes. One of: * - 'main': The main generated module. @@ -694,6 +686,9 @@ function $existing_function_name() { * - 'main': The main generated module. * - 'test': The test module. */ + #[Group('existing')] + #[Group('test')] + #[DataProvider('dataTestModuleWithExistingFunctions')] public function testTestModuleWithExistingServices(string $generated, string $existing_code) { $services_value = [ [ diff --git a/Test/Unit/ComponentTestsPHPUnit9Test.php b/Test/Unit/ComponentTestsPHPUnit9Test.php index 6133c726..c73a8475 100644 --- a/Test/Unit/ComponentTestsPHPUnit9Test.php +++ b/Test/Unit/ComponentTestsPHPUnit9Test.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Fixtures\File\MockableExtension; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; use DrupalCodeBuilder\Test\Unit\Parsing\YamlTester; @@ -69,7 +71,7 @@ function testModuleGenerationTestsWithModuleDependencies() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\Kernel\MyTest'); $php_tester->assertHasMethods(['setUp', 'testMyTest']); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'dependency_one', @@ -150,7 +152,7 @@ function testModuleGenerationKernelTest() { $php_tester->assertDrupalCodingStandards($this->phpcsExcludedSniffs); $php_tester->assertHasClass('Drupal\Tests\test_module\Kernel\MyTest'); $php_tester->assertClassHasParent('Drupal\KernelTests\KernelTestBase'); - $php_tester->assertClassHasProtectedProperty('modules', 'array', [ + $php_tester->assertClassHasProtectedProperty('modules', NULL, [ 'system', 'user', 'test_module', @@ -163,9 +165,8 @@ function testModuleGenerationKernelTest() { /** * Create a kernel test with creation traits. - * - * @group test */ + #[Group('test')] function testKernelTestWithTraits() { // Create a module. $module_name = 'test_module'; @@ -204,9 +205,8 @@ function testKernelTestWithTraits() { /** * Create a test class with services. - * - * @group di */ + #[Group('di')] function testModuleGenerationTestsWithServices() { // Create a module. $module_name = 'test_module'; @@ -261,9 +261,8 @@ function testModuleGenerationTestsWithServices() { /** * Create a test class with a test module. - * - * @group test */ + #[Group('test')] function testModuleGenerationTestsWithBasicTestModule() { // Create a module. $module_name = 'generated_module'; @@ -296,9 +295,8 @@ function testModuleGenerationTestsWithBasicTestModule() { /** * Create a test class with a test module and module components. - * - * @group test */ + #[Group('test')] function testModuleGenerationTestsWithTestModuleComponents() { // TODO: Figure out why this works on 8 but not on 9! $this->markTestSkipped(); @@ -442,7 +440,7 @@ function testModuleGenerationTestsWithTestModuleComponents() { 'generated_module', 'my_test', ]; - $php_tester->assertClassHasProtectedProperty('modules', 'array', $expected_modules_property_value); + $php_tester->assertClassHasProtectedProperty('modules', NULL, $expected_modules_property_value); } /** @@ -478,11 +476,6 @@ public static function dataTestModuleWithExistingFunctions() { * This generates a hook in either the main or the test module, with an * existing function is either the main or test module file. * - * @group existing - * @group test - * - * @dataProvider dataTestModuleWithExistingFunctions - * * @param string $generated * Where the generated hook goes. One of: * - 'main': The main generated module. @@ -492,6 +485,9 @@ public static function dataTestModuleWithExistingFunctions() { * - 'main': The main generated module. * - 'test': The test module. */ + #[Group('existing')] + #[Group('test')] + #[DataProvider('dataTestModuleWithExistingFunctions')] public function testTestModuleWithExistingFunctions(string $generated, string $existing_code) { // Create a module. $module_data = [ @@ -604,11 +600,6 @@ function $existing_function_name() { * This generates a service in either the main or the test module, with an * existing service is either the main or test module. * - * @group existing - * @group test - * - * @dataProvider dataTestModuleWithExistingFunctions - * * @param string $generated * Where the generated service goes. One of: * - 'main': The main generated module. @@ -618,6 +609,9 @@ function $existing_function_name() { * - 'main': The main generated module. * - 'test': The test module. */ + #[Group('existing')] + #[Group('test')] + #[DataProvider('dataTestModuleWithExistingFunctions')] public function testTestModuleWithExistingServices(string $generated, string $existing_code) { $services_value = [ [ diff --git a/Test/Unit/ComponentThemeHook10Test.php b/Test/Unit/ComponentThemeHook10Test.php index cd2bb4e7..2d8635ee 100644 --- a/Test/Unit/ComponentThemeHook10Test.php +++ b/Test/Unit/ComponentThemeHook10Test.php @@ -2,12 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests the theme hook generator class. - * @group hooks */ +#[Group('hooks')] class ComponentThemeHook10Test extends TestBase { /** @@ -32,6 +33,8 @@ function testThemeHook() { 'short_description' => 'Test Module description', 'hooks' => [ ], + // Set this to procedural; the OO version is tested on D11. + 'hook_implementation_type' => 'procedural', 'theme_hooks' => [ $theme_hook_name, ], diff --git a/Test/Unit/ComponentThemeHook11Test.php b/Test/Unit/ComponentThemeHook11Test.php index f70b87cf..16767eb1 100644 --- a/Test/Unit/ComponentThemeHook11Test.php +++ b/Test/Unit/ComponentThemeHook11Test.php @@ -2,12 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; /** * Tests the theme hook generator class. - * @group hooks */ +#[Group('hooks')] class ComponentThemeHook11Test extends TestBase { /** @@ -32,31 +33,37 @@ function testThemeHook() { 'short_description' => 'Test Module description', 'hooks' => [ ], + // Set this to OO only, so we don't have the extra legacy code. + 'hook_implementation_type' => 'oo', 'theme_hooks' => [ $theme_hook_name, ], 'readme' => FALSE, ]; $files = $this->generateModuleFiles($module_data); - $file_names = array_keys($files); - $this->assertCount(3, $files, "Expected number of files is returned."); - $this->assertArrayHasKey("$module_name.info.yml", $files, "The files list has a .info.yml file."); - $this->assertArrayHasKey("$module_name.module", $files, "The files list has a .module file."); - $this->assertArrayHasKey("templates/my-themeable.html.twig", $files, "The files list has a twig file."); + $this->assertFiles([ + 'testmodule.info.yml', + 'templates/my-themeable.html.twig', + 'src/Hook/TestmoduleHooks.php', + ], $files); - // Check the .module file. - $module_file = $files["$module_name.module"]; - $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $module_file); + // Check the hooks file. + $hooks_file = $files['src/Hook/TestmoduleHooks.php']; + $php_tester = PHPTester::fromCodeFile($this->drupalMajorVersion, $hooks_file); $php_tester->assertDrupalCodingStandards(); - $php_tester->assertHasHookImplementation('hook_theme', $module_name); + $php_tester->assertHasMethod('theme'); + // TODO: Attribute testing. + $this->assertStringContainsString("#[Hook('theme')]", $hooks_file); + + $method_tester = $php_tester->getMethodTester('theme'); // Check that the hook_theme() implementation has the generated code. // This covers the specialized HookTheme hook generator class getting used. - $this->assertFunctionCode($module_file, "{$module_name}_theme", "'$theme_hook_name' =>"); - $this->assertFunctionCode($module_file, "{$module_name}_theme", "'render element' => 'elements',"); + $method_tester->assertHasLine("'$theme_hook_name' =>"); + $method_tester->assertHasLine("'render element' => 'elements',"); // TODO: check the other file contents. } diff --git a/Test/Unit/ContainerCollectionTest.php b/Test/Unit/ContainerCollectionTest.php index 5ae4f418..1a8b036e 100644 --- a/Test/Unit/ContainerCollectionTest.php +++ b/Test/Unit/ContainerCollectionTest.php @@ -2,12 +2,12 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; /** * Tests the container collection. - * - * @group container */ +#[Group('container')] class ContainerCollectionTest extends TestBase { protected $drupalMajorVersion = 9; @@ -18,7 +18,10 @@ class ContainerCollectionTest extends TestBase { public function testCollectorServices() { $collect = \DrupalCodeBuilder\Factory::getTask('Collect'); $reflection = new \ReflectionProperty($collect, 'collectors'); - $reflection->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $reflection->setAccessible(TRUE); + } + $collectors = $reflection->getValue($collect); $collector_classes = []; diff --git a/Test/Unit/ContainerRebuildTest.php b/Test/Unit/ContainerRebuildTest.php index 36d4eeae..f7ef77eb 100644 --- a/Test/Unit/ContainerRebuildTest.php +++ b/Test/Unit/ContainerRebuildTest.php @@ -2,12 +2,13 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; /** * Tests rebuilding the cached container. - * @group container */ +#[Group('container')] class ContainerRebuildTest extends TestCase { /** diff --git a/Test/Unit/ContainerTest.php b/Test/Unit/ContainerTest.php index be10e555..d88b22a5 100644 --- a/Test/Unit/ContainerTest.php +++ b/Test/Unit/ContainerTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; /** @@ -10,8 +11,8 @@ * In particular, this needs to test that two separate test cases with different * core major versions set on the environment don't cross-pollute the * container's versionned services. - * @group container */ +#[Group('container')] class ContainerTest extends TestCase { /** @@ -34,7 +35,10 @@ public function testContainer7() { $collect_task = $container->get('Collect'); $collect_reflection = new \ReflectionObject($collect_task); $p = $collect_reflection->getProperty('collectors'); - $p->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $p->setAccessible(TRUE); + } + $collectors = $p->getValue($collect_task); $this->assertCount(1, $collectors); $this->assertArrayHasKey('Collect\HooksCollector', $p->getValue($collect_task)); @@ -61,7 +65,10 @@ public function testContainer8() { $collect_task = $container->get('Collect'); $collect_reflection = new \ReflectionObject($collect_task); $p = $collect_reflection->getProperty('collectors'); - $p->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $p->setAccessible(TRUE); + } + $collectors = $p->getValue($collect_task); $this->assertNotCount(1, $collectors); @@ -70,7 +77,10 @@ public function testContainer8() { $r = new \ReflectionObject($generate_module); $p = $r->getProperty('base'); - $p->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $p->setAccessible(TRUE); + } + $this->assertEquals('module', $p->getValue($generate_module)); $generate_profile = $container->get('Generate|profile'); @@ -78,7 +88,10 @@ public function testContainer8() { $r = new \ReflectionObject($generate_profile); $p = $r->getProperty('base'); - $p->setAccessible(TRUE); + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $p->setAccessible(TRUE); + } + $this->assertEquals('profile', $p->getValue($generate_profile)); } diff --git a/Test/Unit/DrupalCodeBuilderAssertionsTest.php b/Test/Unit/DrupalCodeBuilderAssertionsTest.php index 4ca90cdd..f3cb7e9b 100644 --- a/Test/Unit/DrupalCodeBuilderAssertionsTest.php +++ b/Test/Unit/DrupalCodeBuilderAssertionsTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -15,13 +16,12 @@ class DrupalCodeBuilderAssertionsTest extends TestCase { /** * Tests the assertNoTrailingWhitespace() assertion. * - * @dataProvider providerAssertNoTrailingWhitespace - * * @param $code * The code to test with the assertion. * @param $pass * Whether the assertion is expected to pass (TRUE) or fail (FALSE). */ + #[DataProvider('providerAssertNoTrailingWhitespace')] public function testAssertNoTrailingWhitespace($code, $pass) { try { TestBase::assertNoTrailingWhitespace($code); @@ -54,13 +54,12 @@ public static function providerAssertNoTrailingWhitespace() { /** * Tests the assertFunctionParameter() assertion. * - * @dataProvider providerAssertFunctionParameter - * * @param $code * The code to test with the assertion. * @param $pass * Whether the assertion is expected to pass (TRUE) or fail (FALSE). */ + #[DataProvider('providerAssertFunctionParameter')] public function testAssertFunctionParameter($code, $pass) { // TODO: adapt these to cover PHPTester. $this->markTestSkipped(); @@ -102,8 +101,6 @@ public static function providerAssertFunctionParameter() { /** * Tests the assertDocBlock() assertion. * - * @dataProvider providerAssertDocBlock - * * @param $lines * The docblock lines to test with the assertion. * @param $code @@ -111,6 +108,7 @@ public static function providerAssertFunctionParameter() { * @param $pass * Whether the assertion is expected to pass (TRUE) or fail (FALSE). */ + #[DataProvider('providerAssertDocBlock')] public function testAssertDocBlock($lines, $code, $indent, $pass) { // TODO: adapt these to cover PHPTester. $this->markTestSkipped(); @@ -251,13 +249,12 @@ function myMethod { /** * Tests the assertFunction() assertion. * - * @dataProvider providerAssertFunction - * * @param $code * The code to test with the assertion. * @param $pass * Whether the assertion is expected to pass (TRUE) or fail (FALSE). */ + #[DataProvider('providerAssertFunction')] public function testAssertFunction($code, $pass) { // TODO: adapt these to cover PHPTester. $this->markTestSkipped(); diff --git a/Test/Unit/GenerateHelperComponentCollectorTest.php b/Test/Unit/GenerateHelperComponentCollectorTest.php index 69a97792..42748436 100644 --- a/Test/Unit/GenerateHelperComponentCollectorTest.php +++ b/Test/Unit/GenerateHelperComponentCollectorTest.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\DataProvider; use DrupalCodeBuilder\Definition\MergingGeneratorDefinition; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Definition\DeferredGeneratorDefinition; @@ -180,9 +182,8 @@ public static function providerGeneratorChildNoRequests() { /** * Request with nested component properties. - * - * @dataProvider providerGeneratorChildNoRequests */ + #[DataProvider('providerGeneratorChildNoRequests')] public function testGeneratorChildNoRequests($data_value, $expected_paths) { $definition = MergingGeneratorDefinition::createFromGeneratorType('my_root') ->setName('my_root') @@ -256,9 +257,8 @@ public function testMultipleStringGeneratorChildNoRequests() { /** * Request with only the root generator, with a single-value preset. - * - * @group presets */ + #[Group('presets')] public function testSingleGeneratorSinglePresetsNoRequirements() { $definition = MergingGeneratorDefinition::createFromGeneratorType('my_root') ->setProperties([ @@ -343,9 +343,8 @@ public function testSingleGeneratorSinglePresetsNoRequirements() { /** * Request with only the root generator, with a multi-valued preset. - * - * @group presets */ + #[Group('presets')] public function testSingleGeneratorMultiPresetsNoRequirements() { $definition = MergingGeneratorDefinition::createFromGeneratorType('my_root') ->setProperties([ diff --git a/Test/Unit/ParserDocBlockTest.php b/Test/Unit/ParserDocBlockTest.php index f781e63c..c81aaa2f 100644 --- a/Test/Unit/ParserDocBlockTest.php +++ b/Test/Unit/ParserDocBlockTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; use DrupalCodeBuilder\Test\Unit\Parsing\DocBlockTester; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\ExpectationFailedException; @@ -10,9 +11,8 @@ /** * Unit tests for the DocBlockTester test helper. - * - * @group php_tester_docblocks */ +#[Group('php_tester_docblocks')] class ParserDocBlockTest extends TestCase { /** diff --git a/Test/Unit/ParserPHPMethodTest.php b/Test/Unit/ParserPHPMethodTest.php new file mode 100644 index 00000000..8ae29f7c --- /dev/null +++ b/Test/Unit/ParserPHPMethodTest.php @@ -0,0 +1,81 @@ +getMethodTester('myMethod'); + + // Specifying no expected parameters omit checking parameters entirely. + $this->assertAssertion(TRUE, $method_tester, 'assertHasAttribute', '\Some\Other\Space\AttributeClass'); + $this->assertAssertion(TRUE, $method_tester, 'assertHasAttribute', '\Some\Other\Space\AttributeClass', ['cake']); + + $this->assertAssertion(FALSE, $method_tester, 'assertHasAttribute', '\Some\Other\Space\AttributeClass', ['wrong']); + $this->assertAssertion(FALSE, $method_tester, 'assertHasAttribute', '\Some\Other\Space\AttributeClass', ['cake', 'too many']); + } + + /** + * Helper for tests that test custom assertions. + * + * @param bool $pass + * Whether the assertion should pass with the given parameters: TRUE if it + * should pass, FALSE if it should fail. + * @param object $php_tester + * The PHP tester, on which to call the assertion method. + * @param string $assertion_name + * The name of the assertion method. It is expected to be on the given + * object. + * @param mixed ...$assertion_parameters + * Remaining parameters are passed to the assertion method. + */ + protected function assertAssertion($pass, $php_tester, $assertion_name, ...$assertion_parameters) { + $message_parameters = print_r($assertion_parameters, TRUE); + + try { + $php_tester->$assertion_name(...$assertion_parameters); + + // We get here if the assertion passed. + if (!$pass) { + $this->fail("The assertion {$assertion_name}() should fail with the following parameters: {$message_parameters}"); + } + } + catch (ExpectationFailedException|AssertionFailedError $e) { + // We get here if the assertion failed. + if ($pass) { + $failure_message = $e->getMessage(); + $this->fail("The assertion {$assertion_name}() should pass with the following parameters:\n{$message_parameters}. Got failure:\n{$failure_message}."); + } + } + + // This is just to stop PHPUnit complaining that the test does not perform + // any assertions. + $this->assertTrue(TRUE); + } + +} diff --git a/Test/Unit/ParserPHPTest.php b/Test/Unit/ParserPHPTest.php index abfa9e1e..9b3f0dd2 100644 --- a/Test/Unit/ParserPHPTest.php +++ b/Test/Unit/ParserPHPTest.php @@ -2,6 +2,8 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\ExpectationFailedException; use DrupalCodeBuilder\Test\Unit\Parsing\PHPTester; @@ -17,9 +19,8 @@ class ParserPHPTest extends TestCase { * * This is fairly simple for now, and is just to sanity check that Coder * and PHP Codersniffer run ok. - * - * @dataProvider providerAssertDrupalCodingStandards */ + #[DataProvider('providerAssertDrupalCodingStandards')] public function testAssertDrupalCodingStandards($code, $pass) { $php_tester = new PHPTester(8, $code); @@ -180,9 +181,8 @@ class Foo implements Plain, WithSpace { /** * Tests the assertClassHasInterfaces() assertion. - * - * @dataProvider providerAssertClassInterfaces */ + #[DataProvider('providerAssertClassInterfaces')] public function testAssertClassInterfaces($expected_interfaces, $pass_has, $pass_has_not) { $php = <<assertHasElementName($element_name); diff --git a/Test/Unit/Parsing/PHPMethodTester.php b/Test/Unit/Parsing/PHPMethodTester.php index 98669781..d4cfda40 100644 --- a/Test/Unit/Parsing/PHPMethodTester.php +++ b/Test/Unit/Parsing/PHPMethodTester.php @@ -277,6 +277,60 @@ public function assertReturnType($type, $message = NULL) { Assert::assertEquals($type, $this->methodNode->returnType); } + /** + * Asserts that the method has an attribute of the given class. + * + * @param string $expected_attribute_class + * The full class name of the expected attribute class, WITH the leading '\' + * @param string[] $expected_attribute_parameters + * (optional) An array of the attribute's expected parameters. Only scalar + * parameter values are supported. If omitted, does not assert that there + * are no parameters, but if specified, asserts the size of the array + * matches the number of parameters. + * @param string $message + * (optional) The assertion message. + */ + public function assertHasAttribute(string $expected_attribute_class, array $expected_attribute_parameters = [], $message = NULL) { + assert(str_starts_with($expected_attribute_class, '\\')); + + $message ??= "Attribute $expected_attribute_class not found on the method or function."; + + Assert::assertNotEmpty($this->methodNode->attrGroups, $message); + + // AFAICT, an AttributeGroup only has a single attribute in it, despite the + // class name -- even if there are multiple copies of the same attribute + // class, for instance. + /** @var \PhpParser\Node\AttributeGroup $attribute */ + foreach ($this->methodNode->attrGroups as $attribute) { + if (substr_count($expected_attribute_class, '\\') > 1) { + $full_attribute_class_name = '\\' . $this->fileTester->resolveImportedClassLike($attribute->attrs[0]->name->name); + } + else { + $full_attribute_class_name = '\\' . $attribute->attrs[0]->name->name; + } + + if ($expected_attribute_class === $full_attribute_class_name) { + // Found the attribute. We return out of this condition, so that this + // method can fail if the expected attribute wasn't found among the + // attributes. + if (empty($expected_attribute_parameters)) { + return; + } + + + // Test its parameters. + Assert::assertEquals(count($expected_attribute_parameters), count($attribute->attrs[0]->args)); + foreach ($expected_attribute_parameters as $index => $parameter_value) { + Assert::assertEquals($parameter_value, $attribute->attrs[0]->args[$index]->value->value); + } + + return; + } + } + + Assert::fail($message); + } + /** * Asserts the function body is not empty. * diff --git a/Test/Unit/Parsing/PHPTester.php b/Test/Unit/Parsing/PHPTester.php index 4a958a10..4c38e9a6 100644 --- a/Test/Unit/Parsing/PHPTester.php +++ b/Test/Unit/Parsing/PHPTester.php @@ -121,8 +121,23 @@ public function assertDrupalCodingStandards(array $excluded_sniffs = []) { // to stand out. $excluded_sniffs[] = 'Drupal.Commenting.TodoComment.TodoFormat'; + // Temporarily remove this sniff because it's buggy with PHPCS 4.x. + // @see https://github.com/slevomat/coding-standard/issues/1810 + $excluded_sniffs[] = 'SlevomatCodingStandard.Commenting.ForbiddenComments'; + if (empty($this->phpCodeFilePath)) { - // Exclude this sniff if we don't have access to the file name. + // Exclude the class names sniff if we don't have access to the file name. + $excluded_sniffs[] = 'Squiz.Classes.ClassFileName.NoMatch'; + $excluded_sniffs[] = 'Drupal.Classes.ClassFileName.NoMatch'; + } + + if ($this->drupalMajorVersion <= 7) { + // Code for Drupal 7 and earlier uses long array syntax. + $excluded_sniffs[] = 'Generic.Arrays.DisallowLongArraySyntax'; + + // Drupal 7 typically has classes in .inc files, which do not match the + // class name. + $excluded_sniffs[] = 'Squiz.Classes.ClassFileName.NoMatch'; $excluded_sniffs[] = 'Drupal.Classes.ClassFileName.NoMatch'; } @@ -479,7 +494,7 @@ public function assertInterfaceHasParents($expected_parent_interface_full_names, * @return string * The full name, without the leading '\'. */ - protected function resolveImportedClassLike($name) { + public function resolveImportedClassLike($name) { foreach ($this->parser_nodes['imports'] as $use_node) { if ($use_node->uses[0]->name->getLast() === $name) { return $use_node->uses[0]->name->toString(); @@ -761,7 +776,7 @@ public function assertNotClassHasInterfaces($not_expected_interface_names) { * @param string $message * (optional) The assertion message. */ - public function assertClassHasConstant(string $name, string $message = NULL) { + public function assertClassHasConstant(string $name, ?string $message = NULL) { $message ??= "The class has the constant '$name'."; Assert::assertArrayHasKey($name, $this->parser_nodes['constants'], $message); @@ -772,15 +787,15 @@ public function assertClassHasConstant(string $name, string $message = NULL) { * * @param string $property_name * The name of the property, without the initial '$'. - * @param string $typehint - * The typehint for the property, without the initial '\' if a class or - * interface. + * @param string|null $typehint + * (optional) The typehint for the property, without the initial '\' if a + * class or interface. * @param mixed $default * (optional) The expected default value of the property, as a PHP value. * @param string $message * (optional) The assertion message. */ - public function assertClassHasPublicProperty($property_name, $typehint, $default = NULL, $message = NULL) { + public function assertClassHasPublicProperty($property_name, ?string $typehint = NULL, $default = NULL, $message = NULL) { $message = $message ?? "The class defines the public property \${$property_name}"; $this->assertClassHasProperty($property_name, $typehint, $default, $message); @@ -794,15 +809,15 @@ public function assertClassHasPublicProperty($property_name, $typehint, $default * * @param string $property_name * The name of the property, without the initial '$'. - * @param string $typehint - * The typehint for the property, without the initial '\' if a class or - * interface. + * @param string|null $typehint + * (optional) The typehint for the property, without the initial '\' if a + * class or interface. * @param mixed $default * (optional) The expected default value of the property, as a PHP value. * @param string $message * (optional) The assertion message. */ - public function assertClassHasProtectedProperty($property_name, $typehint, $default = NULL, $message = NULL) { + public function assertClassHasProtectedProperty($property_name, ?string $typehint = NULL, $default = NULL, $message = NULL) { $message = $message ?? "The class defines the protected property \${$property_name}"; $this->assertClassHasProperty($property_name, $typehint, $default, $message); @@ -824,7 +839,7 @@ public function assertClassHasProtectedProperty($property_name, $typehint, $defa * @param string $message * (optional) The assertion message. */ - public function assertClassHasProperty($property_name, $typehint, $default, $message = NULL) { + public function assertClassHasProperty($property_name, ?string $typehint = NULL, $default = NULL, $message = NULL) { $message = $message ?? "The class defines the property \${$property_name}"; Assert::assertArrayHasKey($property_name, $this->parser_nodes['properties'], $message); diff --git a/Test/Unit/RenderAttributesTest.php b/Test/Unit/RenderAttributesTest.php index a9c4c2e7..3b256601 100644 --- a/Test/Unit/RenderAttributesTest.php +++ b/Test/Unit/RenderAttributesTest.php @@ -11,7 +11,7 @@ class RenderAttributesTest extends TestCase { /** - * Tests an annotation with nested annotations. + * Tests an attribute with nested objects. */ public function testAttributeWithNesting() { $attribute = PhpAttributes::class( @@ -26,6 +26,8 @@ public function testAttributeWithNesting() { 'purr' => 'value', ], 'star_trek_name' => "T'Pau", + 'fluffy' => TRUE, + 'class' => '\Drupal\my_module\KittyHandler::class', ], [ 'id' => 'The plugin ID.', @@ -44,6 +46,8 @@ public function testAttributeWithNesting() { $attribute = implode("\n", $lines); + // Note that the class won't get extracted as fully-qualified class + // extraction is handled at the PHP file level. $expected_attribute = << 'value', ], star_trek_name: "T'Pau", + fluffy: TRUE, + class: \Drupal\my_module\KittyHandler::class, )] EOT; $this->assertEquals($expected_attribute, $attribute); } + /** + * Test attributes with inline values. + */ public function testAttributeInline() { // With inline implicit. $attribute = PhpAttributes::class( diff --git a/Test/Unit/TestBase.php b/Test/Unit/TestBase.php index 6d6f77d7..41573c0c 100644 --- a/Test/Unit/TestBase.php +++ b/Test/Unit/TestBase.php @@ -121,7 +121,7 @@ protected function getMockedExtension(string $type, array $files = []) { * @param * An array of files. */ - protected function generateComponentFilesFromData(DataItem $component_data, DrupalExtension $extension = NULL) { + protected function generateComponentFilesFromData(DataItem $component_data, ?DrupalExtension $extension = NULL) { $violations = $component_data->validate(); if ($violations) { @@ -146,7 +146,7 @@ protected function generateComponentFilesFromData(DataItem $component_data, Drup * @param * An array of files. */ - protected function generateModuleFiles(array $module_data, DrupalExtension $extension = NULL) { + protected function generateModuleFiles(array $module_data, ?DrupalExtension $extension = NULL) { $component_data = $this->getRootComponentBlankData('module'); $component_data->set($module_data); diff --git a/Test/Unit/UnitDataItemMergeTest.php b/Test/Unit/UnitDataItemMergeTest.php index 3c0d8aa7..6c30853b 100644 --- a/Test/Unit/UnitDataItemMergeTest.php +++ b/Test/Unit/UnitDataItemMergeTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use DrupalCodeBuilder\Definition\PropertyDefinition; use DrupalCodeBuilder\Exception\MergeDataLossException; @@ -43,9 +44,8 @@ public function testSingleSimpleData() { /** * Tests multi-valued simple data. - * - * @dataProvider dataMultipleSimpleData */ + #[DataProvider('dataMultipleSimpleData')] public function testMultipleSimpleData(array $original_values, array $other_values, ?array $end_values, ?bool $expected_result) { $definition = PropertyDefinition::create('string') ->setMultiple(TRUE); @@ -147,9 +147,8 @@ public static function dataMultipleSimpleData() { /** * Tests single-valued complex data. - * - * @dataProvider dataSingleComplexData */ + #[DataProvider('dataSingleComplexData')] public function testSingleComplexData(array $original_values, array $other_values, ?array $end_values, ?bool $expected_result, bool $expect_exception) { $definition = PropertyDefinition::create('complex') ->setProperties([ @@ -219,9 +218,8 @@ public static function dataSingleComplexData() { /** * Tests multi-valued complex data. - * - * @dataProvider dataMultipleComplexData */ + #[DataProvider('dataMultipleComplexData')] public function testMultipleComplexData(array $original_values, array $other_values, ?array $end_values, ?bool $expected_result) { $definition = PropertyDefinition::create('complex') ->setMultiple(TRUE) @@ -353,9 +351,8 @@ public static function dataMultipleComplexData() { * Tests merging mapping data. * * We only cover single-valued as mapping data is rarely if ever multiple. - * - * @dataProvider dataMappingData */ + #[DataProvider('dataMappingData')] public function testMappingData(array $original_values, array $other_values, ?array $end_values, ?bool $expected_result, bool $expect_exception) { $definition = PropertyDefinition::create('mapping'); diff --git a/Test/Unit/UnitDependencyInjectionAnalysisTest.php b/Test/Unit/UnitDependencyInjectionAnalysisTest.php index 817e5d40..f8f1f8c6 100644 --- a/Test/Unit/UnitDependencyInjectionAnalysisTest.php +++ b/Test/Unit/UnitDependencyInjectionAnalysisTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; /** @@ -12,14 +13,13 @@ class UnitDependencyInjectionAnalysisTest extends TestCase { /** * Tests the method analysis. * - * @dataProvider dataDependencyInjection - * * @param object $class_object * An instance of an anonymous class to analyse. This is an object because * passing the class name of anonymous class is fiddly. * @param array $result * The expected result. */ + #[DataProvider('dataDependencyInjection')] public function testDependencyInjection($class_object, $result) { $parent_construction_parameters = \DrupalCodeBuilder\Utility\CodeAnalysis\DependencyInjection::getInjectedParameters($class_object::class, 0); diff --git a/Test/Unit/UnitMethodCollectorTest.php b/Test/Unit/UnitMethodCollectorTest.php index 1937c2a4..4eb9cead 100644 --- a/Test/Unit/UnitMethodCollectorTest.php +++ b/Test/Unit/UnitMethodCollectorTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use DrupalCodeBuilder\Task\Collect\MethodCollector; use DrupalCodeBuilder\Test\Fixtures\MethodCollectorInterface; @@ -13,9 +14,8 @@ class UnitMethodCollectorTest extends TestCase { /** * Tests the method analysis. - * - * @dataProvider providerMethodCollector */ + #[DataProvider('providerMethodCollector')] public function testMethodCollector($method_name, $declaration_result) { $method_collector = new MethodCollector(); diff --git a/Test/Unit/UnitValidatorTest.php b/Test/Unit/UnitValidatorTest.php index 82e97501..637f2fbf 100644 --- a/Test/Unit/UnitValidatorTest.php +++ b/Test/Unit/UnitValidatorTest.php @@ -2,6 +2,7 @@ namespace DrupalCodeBuilder\Test\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use MutableTypedData\Definition\DataDefinition; use PHPUnit\Framework\TestCase; @@ -33,9 +34,8 @@ public static function providerClassNameValidator() { /** * Tests the class name validator. - * - * @dataProvider providerClassNameValidator */ + #[DataProvider('providerClassNameValidator')] public function testClassNameValidator($value, $expected_pass) { $validator = new \DrupalCodeBuilder\MutableTypedData\Validator\ClassName(); @@ -84,9 +84,8 @@ public static function providerMachineNameValidator() { /** * Tests the machine name validator. - * - * @dataProvider providerMachineNameValidator */ + #[DataProvider('providerMachineNameValidator')] public function testMachineNameValidator($value, $expected_pass) { $validator = new \DrupalCodeBuilder\MutableTypedData\Validator\MachineName(); @@ -166,9 +165,8 @@ public static function providerPluginNameValidator() { /** * Tests the plugin name validator. - * - * @dataProvider providerPluginNameValidator */ + #[DataProvider('providerPluginNameValidator')] public function testPluginNameValidator($value, $expected_pass) { $validator = new \DrupalCodeBuilder\MutableTypedData\Validator\PluginName(); @@ -233,9 +231,8 @@ public static function providerYamlPluginNameValidator() { /** * Tests the YAML plugin name validator. - * - * @dataProvider providerYamlPluginNameValidator */ + #[DataProvider('providerYamlPluginNameValidator')] public function testYamlPluginNameValidator($value, $expected_pass) { $validator = new \DrupalCodeBuilder\MutableTypedData\Validator\YamlPluginName(); diff --git a/Test/sample_hook_definitions/10/element_types_processed.php b/Test/sample_hook_definitions/10/element_types_processed.php index 9443b8d5..a9289bba 100644 --- a/Test/sample_hook_definitions/10/element_types_processed.php +++ b/Test/sample_hook_definitions/10/element_types_processed.php @@ -5,17 +5,23 @@ 'type' => 'machine_name', 'label' => 'machine_name', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/MachineName.php', + 'description' => 'Provides a machine name render element.', ), 'textarea' => array ( 'type' => 'textarea', 'label' => 'textarea', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/Textarea.php', + 'description' => 'Provides a form element for input of multiple-line text.', ), 'textfield' => array ( 'type' => 'textfield', 'label' => 'textfield', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/Textfield.php', + 'description' => 'Provides a one-line text field form element.', ), ); \ No newline at end of file diff --git a/Test/sample_hook_definitions/11/element_types_processed.php b/Test/sample_hook_definitions/11/element_types_processed.php index cda37947..41802ffe 100644 --- a/Test/sample_hook_definitions/11/element_types_processed.php +++ b/Test/sample_hook_definitions/11/element_types_processed.php @@ -5,17 +5,23 @@ 'type' => 'machine_name', 'label' => 'machine_name', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/MachineName.php', + 'description' => 'Provides a machine name render element.', ), 'textarea' => array ( 'type' => 'textarea', 'label' => 'textarea', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/Textarea.php', + 'description' => 'Provides a form element for input of multiple-line text.', ), 'textfield' => array ( 'type' => 'textfield', 'label' => 'textfield', 'form' => true, + 'class_filepath' => 'core/lib/Drupal/Core/Render/Element/Textfield.php', + 'description' => 'Provides a one-line text field form element.', ), ); \ No newline at end of file diff --git a/Test/sample_hook_definitions/11/hooks_processed.php b/Test/sample_hook_definitions/11/hooks_processed.php index 2a6bf81b..6d5ac345 100644 --- a/Test/sample_hook_definitions/11/hooks_processed.php +++ b/Test/sample_hook_definitions/11/hooks_processed.php @@ -16,6 +16,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' // Remove the contextual links on all blocks that provide them. @@ -38,6 +39,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' // Change the title of the specific block. @@ -58,6 +60,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' // Add the \'user\' cache context to some blocks. @@ -80,6 +83,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' // Explicitly enable placeholdering of the specific block. @@ -100,6 +104,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' // Example code that would prevent displaying the \'Powered by Drupal\' block in @@ -126,6 +131,7 @@ ), 'group' => 'block', 'core' => true, + 'original_file_path' => 'core/modules/block/block.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/block.api.php', 'body' => ' foreach ($definitions as $id => $definition) { @@ -134,6 +140,1770 @@ // provided by this custom module. } } +', + ), + ), + 'core:entity' => + array ( + 'hook_entity_access' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_access', + 'definition' => 'function hook_entity_access(\\Drupal\\Core\\Entity\\EntityInterface $entity, $operation, \\Drupal\\Core\\Session\\AccountInterface $account)', + 'description' => 'Control entity operation access.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // No opinion. + return \\Drupal\\Core\\Access\\AccessResult::neutral(); +', + ), + 'hook_ENTITY_TYPE_access' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_access', + 'definition' => 'function hook_ENTITY_TYPE_access(\\Drupal\\Core\\Entity\\EntityInterface $entity, $operation, \\Drupal\\Core\\Session\\AccountInterface $account)', + 'description' => 'Control entity operation access for a specific entity type.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // No opinion. + return \\Drupal\\Core\\Access\\AccessResult::neutral(); +', + ), + 'hook_entity_create_access' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_create_access', + 'definition' => 'function hook_entity_create_access(\\Drupal\\Core\\Session\\AccountInterface $account, array $context, $entity_bundle)', + 'description' => 'Control entity create access.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // No opinion. + return \\Drupal\\Core\\Access\\AccessResult::neutral(); +', + ), + 'hook_ENTITY_TYPE_create_access' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_create_access', + 'definition' => 'function hook_ENTITY_TYPE_create_access(\\Drupal\\Core\\Session\\AccountInterface $account, array $context, $entity_bundle)', + 'description' => 'Control entity create access for a specific entity type.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // No opinion. + return \\Drupal\\Core\\Access\\AccessResult::neutral(); +', + ), + 'hook_entity_type_build' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_type_build', + 'definition' => 'function hook_entity_type_build(array &$entity_types)', + 'description' => 'Add to entity type definitions.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + /** @var \\Drupal\\Core\\Entity\\EntityTypeInterface[] $entity_types */ + // Add a form for a custom node form without overriding the default + // node form. To override the default node form, use hook_entity_type_alter(). + $entity_types[\'node\']->setFormClass(\'my_module_foo\', \'Drupal\\my_module\\NodeFooForm\'); +', + ), + 'hook_entity_type_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_type_alter', + 'definition' => 'function hook_entity_type_alter(array &$entity_types)', + 'description' => 'Alter the entity type definitions.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + /** @var \\Drupal\\Core\\Entity\\EntityTypeInterface[] $entity_types */ + // Set the controller class for nodes to an alternate implementation of the + // Drupal\\Core\\Entity\\EntityStorageInterface interface. + $entity_types[\'node\']->setStorageClass(\'Drupal\\my_module\\MyCustomNodeStorage\'); +', + ), + 'hook_entity_view_mode_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_view_mode_info_alter', + 'definition' => 'function hook_entity_view_mode_info_alter(&$view_modes)', + 'description' => 'Alter the view modes for entity types.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $view_modes[\'user\'][\'full\'][\'status\'] = TRUE; +', + ), + 'hook_entity_bundle_info' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_info', + 'definition' => 'function hook_entity_bundle_info()', + 'description' => 'Describe the bundles for entity types.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $bundles[\'user\'][\'user\'][\'label\'] = t(\'User\'); + return $bundles; +', + ), + 'hook_entity_bundle_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_info_alter', + 'definition' => 'function hook_entity_bundle_info_alter(&$bundles)', + 'description' => 'Alter the bundles for entity types.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $bundles[\'user\'][\'user\'][\'label\'] = t(\'Full account\'); + // Override the bundle class for the "article" node type in a custom module. + $bundles[\'node\'][\'article\'][\'class\'] = \'Drupal\\my_module\\Entity\\Article\'; +', + ), + 'hook_entity_bundle_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_create', + 'definition' => 'function hook_entity_bundle_create($entity_type_id, $bundle)', + 'description' => 'Act on entity_bundle_create().', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // When a new bundle is created, the menu needs to be rebuilt to add the + // Field UI menu item tabs. + \\Drupal::service(\'router.builder\')->setRebuildNeeded(); +', + ), + 'hook_entity_bundle_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_delete', + 'definition' => 'function hook_entity_bundle_delete($entity_type_id, $bundle)', + 'description' => 'Act on entity_bundle_delete().', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Remove the settings associated with the bundle in my_module.settings. + $config = \\Drupal::config(\'my_module.settings\'); + $bundle_settings = $config->get(\'bundle_settings\'); + if (isset($bundle_settings[$entity_type_id][$bundle])) { + unset($bundle_settings[$entity_type_id][$bundle]); + $config->set(\'bundle_settings\', $bundle_settings); + } +', + ), + 'hook_entity_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_create', + 'definition' => 'function hook_entity_create(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Acts when creating a new entity.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + \\Drupal::logger(\'example\')->info(\'Entity created: @label\', [\'@label\' => $entity->label()]); +', + ), + 'hook_ENTITY_TYPE_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_create', + 'definition' => 'function hook_ENTITY_TYPE_create(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Acts when creating a new entity of a specific type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + \\Drupal::logger(\'example\')->info(\'ENTITY_TYPE created: @label\', [\'@label\' => $entity->label()]); +', + ), + 'hook_entity_revision_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_revision_create', + 'definition' => 'function hook_entity_revision_create(\\Drupal\\Core\\Entity\\EntityInterface $new_revision, \\Drupal\\Core\\Entity\\EntityInterface $entity, $keep_untranslatable_fields)', + 'description' => 'Respond to entity revision creation.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Retain the value from an untranslatable field, which are by default + // synchronized from the default revision. + $new_revision->set(\'untranslatable_field\', $entity->get(\'untranslatable_field\')); +', + ), + 'hook_ENTITY_TYPE_revision_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_revision_create', + 'definition' => 'function hook_ENTITY_TYPE_revision_create(\\Drupal\\Core\\Entity\\EntityInterface $new_revision, \\Drupal\\Core\\Entity\\EntityInterface $entity, $keep_untranslatable_fields)', + 'description' => 'Respond to entity revision creation.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Retain the value from an untranslatable field, which are by default + // synchronized from the default revision. + $new_revision->set(\'untranslatable_field\', $entity->get(\'untranslatable_field\')); +', + ), + 'hook_entity_preload' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_preload', + 'definition' => 'function hook_entity_preload(array $ids, $entity_type_id)', + 'description' => 'Act on an array of entity IDs before they are loaded.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $entities = []; + + foreach ($ids as $id) { + $entities[] = my_module_swap_revision($id); + } + + return $entities; +', + ), + 'hook_entity_load' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_load', + 'definition' => 'function hook_entity_load(array $entities, $entity_type_id)', + 'description' => 'Act on entities when loaded.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + foreach ($entities as $entity) { + $entity->foo = my_module_add_something($entity); + } +', + ), + 'hook_ENTITY_TYPE_load' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_load', + 'definition' => 'function hook_ENTITY_TYPE_load($entities)', + 'description' => 'Act on entities of a specific type when loaded.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + foreach ($entities as $entity) { + $entity->foo = my_module_add_something($entity); + } +', + ), + 'hook_entity_storage_load' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_storage_load', + 'definition' => 'function hook_entity_storage_load(array $entities, $entity_type)', + 'description' => 'Act on content entities when loaded from the storage.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + foreach ($entities as $entity) { + $entity->foo = my_module_add_something_uncached($entity); + } +', + ), + 'hook_ENTITY_TYPE_storage_load' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_storage_load', + 'definition' => 'function hook_ENTITY_TYPE_storage_load(array $entities)', + 'description' => 'Act on content entities of a given type when loaded from the storage.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + foreach ($entities as $entity) { + $entity->foo = my_module_add_something_uncached($entity); + } +', + ), + 'hook_entity_presave' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_presave', + 'definition' => 'function hook_entity_presave(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Act on an entity before it is created or updated.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($entity instanceof \\Drupal\\Core\\Entity\\ContentEntityInterface && $entity->isTranslatable()) { + $route_match = \\Drupal::routeMatch(); + \\Drupal::service(\'content_translation.synchronizer\')->synchronizeFields($entity, $entity->language()->getId(), $route_match->getParameter(\'source_langcode\')); + } +', + ), + 'hook_ENTITY_TYPE_presave' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_presave', + 'definition' => 'function hook_ENTITY_TYPE_presave(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Act on a specific type of entity before it is created or updated.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($entity->isTranslatable()) { + $route_match = \\Drupal::routeMatch(); + \\Drupal::service(\'content_translation.synchronizer\')->synchronizeFields($entity, $entity->language()->getId(), $route_match->getParameter(\'source_langcode\')); + } +', + ), + 'hook_entity_insert' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_insert', + 'definition' => 'function hook_entity_insert(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to creation of a new entity.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Insert the new entity into a fictional table of all entities. + \\Drupal::database()->insert(\'example_entity\') + ->fields([ + \'type\' => $entity->getEntityTypeId(), + \'id\' => $entity->id(), + \'created\' => \\Drupal::time()->getRequestTime(), + \'updated\' => \\Drupal::time()->getRequestTime(), + ]) + ->execute(); +', + ), + 'hook_ENTITY_TYPE_insert' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_insert', + 'definition' => 'function hook_ENTITY_TYPE_insert(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to creation of a new entity of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Insert the new entity into a fictional table of this type of entity. + \\Drupal::database()->insert(\'example_entity\') + ->fields([ + \'id\' => $entity->id(), + \'created\' => \\Drupal::time()->getRequestTime(), + \'updated\' => \\Drupal::time()->getRequestTime(), + ]) + ->execute(); +', + ), + 'hook_entity_update' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_update', + 'definition' => 'function hook_entity_update(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to updates to an entity.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Update the entity\'s entry in a fictional table of all entities. + \\Drupal::database()->update(\'example_entity\') + ->fields([ + \'updated\' => \\Drupal::time()->getRequestTime(), + ]) + ->condition(\'type\', $entity->getEntityTypeId()) + ->condition(\'id\', $entity->id()) + ->execute(); +', + ), + 'hook_ENTITY_TYPE_update' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_update', + 'definition' => 'function hook_ENTITY_TYPE_update(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to updates to an entity of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Update the entity\'s entry in a fictional table of this type of entity. + \\Drupal::database()->update(\'example_entity\') + ->fields([ + \'updated\' => \\Drupal::time()->getRequestTime(), + ]) + ->condition(\'id\', $entity->id()) + ->execute(); +', + ), + 'hook_entity_translation_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_translation_create', + 'definition' => 'function hook_entity_translation_create(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Acts when creating a new entity translation.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + \\Drupal::logger(\'example\')->info(\'Entity translation created: @label\', [\'@label\' => $translation->label()]); +', + ), + 'hook_ENTITY_TYPE_translation_create' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_translation_create', + 'definition' => 'function hook_ENTITY_TYPE_translation_create(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Acts when creating a new entity translation of a specific type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + \\Drupal::logger(\'example\')->info(\'ENTITY_TYPE translation created: @label\', [\'@label\' => $translation->label()]); +', + ), + 'hook_entity_translation_insert' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_translation_insert', + 'definition' => 'function hook_entity_translation_insert(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Respond to creation of a new entity translation.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $variables = [ + \'@language\' => $translation->language()->getName(), + \'@label\' => $translation->getUntranslated()->label(), + ]; + \\Drupal::logger(\'example\')->notice(\'The @language translation of @label has just been stored.\', $variables); +', + ), + 'hook_ENTITY_TYPE_translation_insert' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_translation_insert', + 'definition' => 'function hook_ENTITY_TYPE_translation_insert(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Respond to creation of a new entity translation of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $variables = [ + \'@language\' => $translation->language()->getName(), + \'@label\' => $translation->getUntranslated()->label(), + ]; + \\Drupal::logger(\'example\')->notice(\'The @language translation of @label has just been stored.\', $variables); +', + ), + 'hook_entity_translation_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_translation_delete', + 'definition' => 'function hook_entity_translation_delete(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Respond to entity translation deletion.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $variables = [ + \'@language\' => $translation->language()->getName(), + \'@label\' => $translation->label(), + ]; + \\Drupal::logger(\'example\')->notice(\'The @language translation of @label has just been deleted.\', $variables); +', + ), + 'hook_ENTITY_TYPE_translation_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_translation_delete', + 'definition' => 'function hook_ENTITY_TYPE_translation_delete(\\Drupal\\Core\\Entity\\EntityInterface $translation)', + 'description' => 'Respond to entity translation deletion of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $variables = [ + \'@language\' => $translation->language()->getName(), + \'@label\' => $translation->label(), + ]; + \\Drupal::logger(\'example\')->notice(\'The @language translation of @label has just been deleted.\', $variables); +', + ), + 'hook_entity_predelete' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_predelete', + 'definition' => 'function hook_entity_predelete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Act before entity deletion.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $connection = \\Drupal::database(); + // Count references to this entity in a custom table before they are removed + // upon entity deletion. + $id = $entity->id(); + $type = $entity->getEntityTypeId(); + $count = \\Drupal::database()->select(\'example_entity_data\') + ->condition(\'type\', $type) + ->condition(\'id\', $id) + ->countQuery() + ->execute() + ->fetchField(); + + // Log the count in a table that records this statistic for deleted entities. + $connection->merge(\'example_deleted_entity_statistics\') + ->keys([\'type\' => $type, \'id\' => $id]) + ->fields([\'count\' => $count]) + ->execute(); +', + ), + 'hook_ENTITY_TYPE_predelete' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_predelete', + 'definition' => 'function hook_ENTITY_TYPE_predelete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Act before entity deletion of a particular entity type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $connection = \\Drupal::database(); + // Count references to this entity in a custom table before they are removed + // upon entity deletion. + $id = $entity->id(); + $type = $entity->getEntityTypeId(); + $count = \\Drupal::database()->select(\'example_entity_data\') + ->condition(\'type\', $type) + ->condition(\'id\', $id) + ->countQuery() + ->execute() + ->fetchField(); + + // Log the count in a table that records this statistic for deleted entities. + $connection->merge(\'example_deleted_entity_statistics\') + ->keys([\'type\' => $type, \'id\' => $id]) + ->fields([\'count\' => $count]) + ->execute(); +', + ), + 'hook_entity_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_delete', + 'definition' => 'function hook_entity_delete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to entity deletion.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Delete the entity\'s entry from a fictional table of all entities. + \\Drupal::database()->delete(\'example_entity\') + ->condition(\'type\', $entity->getEntityTypeId()) + ->condition(\'id\', $entity->id()) + ->execute(); +', + ), + 'hook_ENTITY_TYPE_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_delete', + 'definition' => 'function hook_ENTITY_TYPE_delete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to entity deletion of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Delete the entity\'s entry from a fictional table of all entities. + \\Drupal::database()->delete(\'example_entity\') + ->condition(\'type\', $entity->getEntityTypeId()) + ->condition(\'id\', $entity->id()) + ->execute(); +', + ), + 'hook_entity_revision_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_revision_delete', + 'definition' => 'function hook_entity_revision_delete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to entity revision deletion.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $referenced_files_by_field = _editor_get_file_uuids_by_field($entity); + foreach ($referenced_files_by_field as $field => $uuids) { + _editor_delete_file_usage($uuids, $entity, 1); + } +', + ), + 'hook_ENTITY_TYPE_revision_delete' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_revision_delete', + 'definition' => 'function hook_ENTITY_TYPE_revision_delete(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Respond to entity revision deletion of a particular type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $referenced_files_by_field = _editor_get_file_uuids_by_field($entity); + foreach ($referenced_files_by_field as $field => $uuids) { + _editor_delete_file_usage($uuids, $entity, 1); + } +', + ), + 'hook_entity_view' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_view', + 'definition' => 'function hook_entity_view(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, \\Drupal\\Core\\Entity\\Display\\EntityViewDisplayInterface $display, $view_mode)', + 'description' => 'Act on entities being assembled before rendering.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Only do the extra work if the component is configured to be displayed. + // This assumes a \'my_module_addition\' extra field has been defined for the + // entity bundle in hook_entity_extra_field_info(). + if ($display->getComponent(\'my_module_addition\')) { + $build[\'my_module_addition\'] = [ + \'#markup\' => my_module_addition($entity), + \'#theme\' => \'my_module_my_additional_field\', + ]; + } +', + ), + 'hook_ENTITY_TYPE_view' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_view', + 'definition' => 'function hook_ENTITY_TYPE_view(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, \\Drupal\\Core\\Entity\\Display\\EntityViewDisplayInterface $display, $view_mode)', + 'description' => 'Act on entities of a particular type being assembled before rendering.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Only do the extra work if the component is configured to be displayed. + // This assumes a \'my_module_addition\' extra field has been defined for the + // entity bundle in hook_entity_extra_field_info(). + if ($display->getComponent(\'my_module_addition\')) { + $build[\'my_module_addition\'] = [ + \'#markup\' => my_module_addition($entity), + \'#theme\' => \'my_module_my_additional_field\', + ]; + } +', + ), + 'hook_entity_view_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_view_alter', + 'definition' => 'function hook_entity_view_alter(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, \\Drupal\\Core\\Entity\\Display\\EntityViewDisplayInterface $display)', + 'description' => 'Alter the results of the entity build array.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($build[\'#view_mode\'] == \'full\' && isset($build[\'an_additional_field\'])) { + // Change its weight. + $build[\'an_additional_field\'][\'#weight\'] = -10; + + // Add a #post_render callback to act on the rendered HTML of the entity. + // The object must implement \\Drupal\\Core\\Security\\TrustedCallbackInterface. + $build[\'#post_render\'][] = \'\\Drupal\\my_module\\NodeCallback::postRender\'; + } +', + ), + 'hook_ENTITY_TYPE_view_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_view_alter', + 'definition' => 'function hook_ENTITY_TYPE_view_alter(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, \\Drupal\\Core\\Entity\\Display\\EntityViewDisplayInterface $display)', + 'description' => 'Alter the results of the entity build array for a particular entity type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($build[\'#view_mode\'] == \'full\' && isset($build[\'an_additional_field\'])) { + // Change its weight. + $build[\'an_additional_field\'][\'#weight\'] = -10; + + // Add a #post_render callback to act on the rendered HTML of the entity. + $build[\'#post_render\'][] = \'my_module_node_post_render\'; + } +', + ), + 'hook_entity_prepare_view' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_prepare_view', + 'definition' => 'function hook_entity_prepare_view($entity_type_id, array $entities, array $displays, $view_mode)', + 'description' => 'Act on entities as they are being prepared for view.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Load a specific node into the user object for later theming. + if (!empty($entities) && $entity_type_id == \'user\') { + // Only do the extra work if the component is configured to be + // displayed. This assumes a \'my_module_addition\' extra field has been + // defined for the entity bundle in hook_entity_extra_field_info(). + $ids = []; + foreach ($entities as $id => $entity) { + if ($displays[$entity->bundle()]->getComponent(\'my_module_addition\')) { + $ids[] = $id; + } + } + if ($ids) { + $nodes = my_module_get_user_nodes($ids); + foreach ($ids as $id) { + $entities[$id]->user_node = $nodes[$id]; + } + } + } +', + ), + 'hook_entity_view_mode_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_view_mode_alter', + 'definition' => 'function hook_entity_view_mode_alter(&$view_mode, \\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Change the view mode of an entity that is being displayed.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // For nodes, change the view mode when it is teaser. + if ($entity->getEntityTypeId() == \'node\' && $view_mode == \'teaser\') { + $view_mode = \'my_custom_view_mode\'; + } +', + ), + 'hook_ENTITY_TYPE_view_mode_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_view_mode_alter', + 'definition' => 'function hook_ENTITY_TYPE_view_mode_alter(string &$view_mode, \\Drupal\\Core\\Entity\\EntityInterface $entity): void', + 'description' => 'Change the view mode of a specific entity type currently being displayed.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Change the view mode to teaser. + if ($view_mode == \'full\') { + $view_mode = \'teaser\'; + } +', + ), + 'hook_ENTITY_TYPE_build_defaults_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_build_defaults_alter', + 'definition' => 'function hook_ENTITY_TYPE_build_defaults_alter(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, $view_mode)', + 'description' => 'Alter entity renderable values before cache checking during rendering.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + +', + ), + 'hook_entity_build_defaults_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_build_defaults_alter', + 'definition' => 'function hook_entity_build_defaults_alter(array &$build, \\Drupal\\Core\\Entity\\EntityInterface $entity, $view_mode)', + 'description' => 'Alter entity renderable values before cache checking during rendering.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + +', + ), + 'hook_entity_view_display_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_view_display_alter', + 'definition' => 'function hook_entity_view_display_alter(\\Drupal\\Core\\Entity\\Display\\EntityViewDisplayInterface $display, array $context)', + 'description' => 'Alter the settings used for displaying an entity.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Leave field labels out of the search index. + if ($context[\'entity_type\'] == \'node\' && $context[\'view_mode\'] == \'search_index\') { + foreach ($display->getComponents() as $name => $options) { + if (isset($options[\'label\'])) { + $options[\'label\'] = \'hidden\'; + $display->setComponent($name, $options); + } + } + } +', + ), + 'hook_entity_display_build_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_display_build_alter', + 'definition' => 'function hook_entity_display_build_alter(&$build, $context)', + 'description' => 'Alter the render array generated by an EntityDisplay for an entity.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + /** @var \\Drupal\\Core\\Entity\\\\Drupal\\Core\\Entity\\ContentEntityInterface $entity */ + $entity = $context[\'entity\']; + if ($entity->getEntityTypeId() === \'my_entity\' && $entity->bundle() === \'display_build_alter_bundle\') { + $build[\'entity_display_build_alter\'][\'#markup\'] = \'Content added in hook_entity_display_build_alter for entity id \' . $entity->id(); + } +', + ), + 'hook_entity_prepare_form' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_prepare_form', + 'definition' => 'function hook_entity_prepare_form(\\Drupal\\Core\\Entity\\EntityInterface $entity, $operation, \\Drupal\\Core\\Form\\FormStateInterface $form_state)', + 'description' => 'Acts on an entity object about to be shown on an entity form.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($operation == \'edit\') { + $entity->label->value = \'Altered label\'; + $form_state->set(\'label_altered\', TRUE); + } +', + ), + 'hook_ENTITY_TYPE_prepare_form' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_prepare_form', + 'definition' => 'function hook_ENTITY_TYPE_prepare_form(\\Drupal\\Core\\Entity\\EntityInterface $entity, $operation, \\Drupal\\Core\\Form\\FormStateInterface $form_state)', + 'description' => 'Acts on a particular type of entity object about to be in an entity form.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($operation == \'edit\') { + $entity->label->value = \'Altered label\'; + $form_state->set(\'label_altered\', TRUE); + } +', + ), + 'hook_entity_form_mode_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_form_mode_alter', + 'definition' => 'function hook_entity_form_mode_alter(&$form_mode, \\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Change the form mode used to build an entity form.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Change the form mode for users with Administrator role. + if ($entity->getEntityTypeId() == \'user\' && $entity->hasRole(\'administrator\')) { + $form_mode = \'my_custom_form_mode\'; + } +', + ), + 'hook_ENTITY_TYPE_form_mode_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_form_mode_alter', + 'definition' => 'function hook_ENTITY_TYPE_form_mode_alter(string &$form_mode, \\Drupal\\Core\\Entity\\EntityInterface $entity): void', + 'description' => 'Change the form mode of a specific entity type currently being displayed.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Change the form mode for nodes with \'article\' bundle. + if ($entity->bundle() == \'article\') { + $form_mode = \'custom_article_form_mode\'; + } +', + ), + 'hook_entity_form_display_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_form_display_alter', + 'definition' => 'function hook_entity_form_display_alter(\\Drupal\\Core\\Entity\\Display\\EntityFormDisplayInterface $form_display, array $context)', + 'description' => 'Alter the settings used for displaying an entity form.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Hide the \'user_picture\' field from the register form. + if ($context[\'entity_type\'] == \'user\' && $context[\'form_mode\'] == \'register\') { + $form_display->setComponent(\'user_picture\', [ + \'region\' => \'hidden\', + ]); + } +', + ), + 'hook_entity_base_field_info' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_base_field_info', + 'definition' => 'function hook_entity_base_field_info(\\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type)', + 'description' => 'Provides custom base field definitions for a content entity type.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($entity_type->id() == \'node\') { + $fields = []; + $fields[\'my_module_text\'] = \\Drupal\\Core\\Field\\BaseFieldDefinition::create(\'string\') + ->setLabel(t(\'The text\')) + ->setDescription(t(\'A text property added by my_module.\')) + ->setComputed(TRUE) + ->setClass(\'\\Drupal\\my_module\\EntityComputedText\'); + + return $fields; + } +', + ), + 'hook_entity_base_field_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_base_field_info_alter', + 'definition' => 'function hook_entity_base_field_info_alter(&$fields, \\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type)', + 'description' => 'Alter base field definitions for a content entity type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Alter the my_module_text field to use a custom class. + if ($entity_type->id() == \'node\' && !empty($fields[\'my_module_text\'])) { + $fields[\'my_module_text\']->setClass(\'\\Drupal\\another_module\\EntityComputedText\'); + } +', + ), + 'hook_entity_bundle_field_info' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_field_info', + 'definition' => 'function hook_entity_bundle_field_info(\\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions)', + 'description' => 'Provides field definitions for a specific bundle within an entity type.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Add a property only to nodes of the \'article\' bundle. + if ($entity_type->id() == \'node\' && $bundle == \'article\') { + $fields = []; + $storage_definitions = my_module_entity_field_storage_info($entity_type); + $fields[\'my_module_bundle_field\'] = \\Drupal\\Core\\Field\\FieldDefinition::createFromFieldStorageDefinition($storage_definitions[\'my_module_bundle_field\']) + ->setLabel(t(\'Bundle Field\')); + return $fields; + } + +', + ), + 'hook_entity_bundle_field_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_bundle_field_info_alter', + 'definition' => 'function hook_entity_bundle_field_info_alter(&$fields, \\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type, $bundle)', + 'description' => 'Alter bundle field definitions.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($entity_type->id() == \'node\' && $bundle == \'article\' && !empty($fields[\'my_module_text\'])) { + // Alter the my_module_text field to use a custom class. + $fields[\'my_module_text\']->setClass(\'\\Drupal\\another_module\\EntityComputedText\'); + } +', + ), + 'hook_entity_field_storage_info' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_field_storage_info', + 'definition' => 'function hook_entity_field_storage_info(\\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type)', + 'description' => 'Provides field storage definitions for a content entity type.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if (\\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof \\Drupal\\Core\\Entity\\DynamicallyFieldableEntityStorageInterface) { + // Query by filtering on the ID as this is more efficient than filtering + // on the entity_type property directly. + $ids = \\Drupal::entityQuery(\'field_storage_config\') + ->condition(\'id\', $entity_type->id() . \'.\', \'STARTS_WITH\') + ->execute(); + // Fetch all fields and key them by field name. + $field_storages = FieldStorageConfig::loadMultiple($ids); + $result = []; + foreach ($field_storages as $field_storage) { + $result[$field_storage->getName()] = $field_storage; + } + + return $result; + } +', + ), + 'hook_entity_field_storage_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_field_storage_info_alter', + 'definition' => 'function hook_entity_field_storage_info_alter(&$fields, \\Drupal\\Core\\Entity\\EntityTypeInterface $entity_type)', + 'description' => 'Alter field storage definitions for a content entity type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Alter the max_length setting. + if ($entity_type->id() == \'node\' && !empty($fields[\'my_module_text\'])) { + $fields[\'my_module_text\']->setSetting(\'max_length\', 128); + } +', + ), + 'hook_entity_operation' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_operation', + 'definition' => 'function hook_entity_operation(\\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Declares entity operations.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $operations = []; + $operations[\'translate\'] = [ + \'title\' => t(\'Translate\'), + \'url\' => \\Drupal\\Core\\Url::fromRoute(\'foo_module.entity.translate\'), + \'weight\' => 50, + ]; + + return $operations; +', + ), + 'hook_entity_operation_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_operation_alter', + 'definition' => 'function hook_entity_operation_alter(array &$operations, \\Drupal\\Core\\Entity\\EntityInterface $entity)', + 'description' => 'Alter entity operations.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Alter the title and weight. + $operations[\'translate\'][\'title\'] = t(\'Translate @entity_type\', [ + \'@entity_type\' => $entity->getEntityTypeId(), + ]); + $operations[\'translate\'][\'weight\'] = 99; +', + ), + 'hook_entity_field_access' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_field_access', + 'definition' => 'function hook_entity_field_access($operation, \\Drupal\\Core\\Field\\FieldDefinitionInterface $field_definition, \\Drupal\\Core\\Session\\AccountInterface $account, ?\\Drupal\\Core\\Field\\FieldItemListInterface $items = NULL)', + 'description' => 'Control access to fields.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($field_definition->getName() == \'field_of_interest\' && $operation == \'edit\') { + return \\Drupal\\Core\\Access\\AccessResult::allowedIfHasPermission($account, \'update field of interest\'); + } + return \\Drupal\\Core\\Access\\AccessResult::neutral(); +', + ), + 'hook_entity_field_access_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_field_access_alter', + 'definition' => 'function hook_entity_field_access_alter(array &$grants, array $context)', + 'description' => 'Alter the default access behavior for a given field.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + /** @var \\Drupal\\Core\\Field\\FieldDefinitionInterface $field_definition */ + $field_definition = $context[\'field_definition\']; + if ($field_definition->getName() == \'field_of_interest\' && $grants[\'node\']->isForbidden()) { + // Override node module\'s restriction to no opinion (neither allowed nor + // forbidden). We don\'t want to provide our own access hook, we only want to + // take out node module\'s part in the access handling of this field. We also + // don\'t want to switch node module\'s grant to + // AccessResultInterface::isAllowed() , because the grants of other modules + // should still decide on their own if this field is accessible or not + $grants[\'node\'] = \\Drupal\\Core\\Access\\AccessResult::neutral()->inheritCacheability($grants[\'node\']); + } +', + ), + 'hook_entity_field_values_init' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_field_values_init', + 'definition' => 'function hook_entity_field_values_init(\\Drupal\\Core\\Entity\\FieldableEntityInterface $entity)', + 'description' => 'Acts when initializing a fieldable entity object.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($entity instanceof \\Drupal\\Core\\Entity\\\\Drupal\\Core\\Entity\\ContentEntityInterface && !$entity->foo->value) { + $entity->foo->value = \'some_initial_value\'; + } +', + ), + 'hook_ENTITY_TYPE_field_values_init' => + array ( + 'type' => 'hook', + 'name' => 'hook_ENTITY_TYPE_field_values_init', + 'definition' => 'function hook_ENTITY_TYPE_field_values_init(\\Drupal\\Core\\Entity\\FieldableEntityInterface $entity)', + 'description' => 'Acts when initializing a fieldable entity object.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if (!$entity->foo->value) { + $entity->foo->value = \'some_initial_value\'; + } +', + ), + 'hook_entity_extra_field_info' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_extra_field_info', + 'definition' => 'function hook_entity_extra_field_info()', + 'description' => 'Exposes "pseudo-field" components on content entities.', + 'destination' => '%module.module', + 'has_return' => true, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $extra = []; + $module_language_enabled = \\Drupal::moduleHandler()->moduleExists(\'language\'); + $description = t(\'Node module element\'); + + foreach (\\Drupal\\node\\Entity\\NodeType::loadMultiple() as $bundle) { + + // Add also the \'language\' select if Language module is enabled and the + // bundle has multilingual support. + // Visibility of the ordering of the language selector is the same as on the + // node/add form. + if ($module_language_enabled) { + $configuration = \\Drupal\\language\\Entity\\ContentLanguageSettings::loadByEntityTypeBundle(\'node\', $bundle->id()); + if ($configuration->isLanguageAlterable()) { + $extra[\'node\'][$bundle->id()][\'form\'][\'language\'] = [ + \'label\' => t(\'Language\'), + \'description\' => $description, + \'weight\' => 0, + ]; + } + } + $extra[\'node\'][$bundle->id()][\'display\'][\'language\'] = [ + \'label\' => t(\'Language\'), + \'description\' => $description, + \'weight\' => 0, + \'visible\' => FALSE, + ]; + } + + return $extra; +', + ), + 'hook_entity_extra_field_info_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_extra_field_info_alter', + 'definition' => 'function hook_entity_extra_field_info_alter(&$info)', + 'description' => 'Alter "pseudo-field" components on content entities.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + // Force node title to always be at the top of the list by default. + foreach (\\Drupal\\node\\Entity\\NodeType::loadMultiple() as $bundle) { + if (isset($info[\'node\'][$bundle->id()][\'form\'][\'title\'])) { + $info[\'node\'][$bundle->id()][\'form\'][\'title\'][\'weight\'] = -20; + } + } +', + ), + 'hook_entity_query_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_query_alter', + 'definition' => 'function hook_entity_query_alter(\\Drupal\\Core\\Entity\\Query\\QueryInterface $query): void', + 'description' => 'Alter an entity query.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + if ($query->hasTag(\'entity_reference\')) { + $entityType = \\Drupal::entityTypeManager()->getDefinition($query->getEntityTypeId()); + $query->sort($entityType->getKey(\'id\'), \'desc\'); + } +', + ), + 'hook_entity_query_ENTITY_TYPE_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_query_ENTITY_TYPE_alter', + 'definition' => 'function hook_entity_query_ENTITY_TYPE_alter(\\Drupal\\Core\\Entity\\Query\\QueryInterface $query): void', + 'description' => 'Alter an entity query for a specific entity type.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $query->condition(\'id\', \'1\', \'<>\'); +', + ), + 'hook_entity_query_tag__TAG_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_query_tag__TAG_alter', + 'definition' => 'function hook_entity_query_tag__TAG_alter(\\Drupal\\Core\\Entity\\Query\\QueryInterface $query): void', + 'description' => 'Alter an entity query that has a specific tag.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $entityType = \\Drupal::entityTypeManager()->getDefinition($query->getEntityTypeId()); + $query->sort($entityType->getKey(\'id\'), \'desc\'); +', + ), + 'hook_entity_query_tag__ENTITY_TYPE__TAG_alter' => + array ( + 'type' => 'hook', + 'name' => 'hook_entity_query_tag__ENTITY_TYPE__TAG_alter', + 'definition' => 'function hook_entity_query_tag__ENTITY_TYPE__TAG_alter(\\Drupal\\Core\\Entity\\Query\\QueryInterface $query): void', + 'description' => 'Alter an entity query for a specific entity type that has a specific tag.', + 'destination' => '%module.module', + 'has_return' => false, + 'procedural' => false, + 'dependencies' => + array ( + ), + 'group' => 'core:entity', + 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Entity/entity.api.php', + 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_entity.api.php', + 'body' => ' + $query->condition(\'id\', \'1\', \'<>\'); ', ), ), @@ -155,6 +1925,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' $node_storage = \\Drupal::entityTypeManager()->getStorage(\'node\'); @@ -212,6 +1983,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' if ($success) { @@ -253,6 +2025,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' // Inject any new status messages into the content area. @@ -275,6 +2048,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' if (isset($form[\'type\']) && $form[\'type\'][\'#value\'] . \'_node_settings\' == $form_id) { @@ -304,6 +2078,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' // Modification for the form with the given form ID goes here. For example, if @@ -332,6 +2107,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' // Modification for the form with the given BASE_FORM_ID goes here. For @@ -360,6 +2136,7 @@ ), 'group' => 'core:form', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Form/form.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_form.api.php', 'body' => ' ', @@ -381,6 +2158,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' $hooks[\'token_info\'] = [ @@ -406,6 +2184,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' if ($hook == \'form_alter\') { @@ -434,6 +2213,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Only fill this in if the .info.yml file does not define a \'datestamp\'. @@ -456,6 +2236,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' my_module_cache_clear(); @@ -475,6 +2256,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' if (in_array(\'lousy_module\', $modules)) { @@ -499,6 +2281,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Set general module variables. @@ -519,6 +2302,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' my_module_cache_clear(); @@ -538,6 +2322,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' if (in_array(\'lousy_module\', $modules)) { @@ -563,6 +2348,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Delete remaining general module variables. @@ -583,6 +2369,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Here, we define a variable to allow tasks to indicate that a particular, @@ -659,6 +2446,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Replace the entire site configuration form provided by Drupal core @@ -680,6 +2468,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // For non-batch updates, the signature can simply be: @@ -750,6 +2539,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Example of updating some content. @@ -782,6 +2572,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' return [ @@ -805,6 +2596,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Indicate that the my_module_update_8001() function provided by this module @@ -840,6 +2632,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // We\'ve removed the 8.x-1.x version of my_module, including database updates. @@ -861,6 +2654,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' return [ @@ -891,6 +2685,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Adjust weight so that the theme Updater gets a chance to handle a given @@ -912,6 +2707,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' $requirements = []; @@ -972,6 +2768,7 @@ ), 'group' => 'core:module', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Extension/module.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_module.api.php', 'body' => ' // Change the title from \'PHP\' to \'PHP version\'. @@ -1001,6 +2798,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Add a checkbox to toggle the breadcrumb trail. @@ -1026,6 +2824,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' static $hooks; @@ -1075,6 +2874,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // This example is from node_preprocess_html(). It adds the node type to @@ -1100,6 +2900,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $suggestions = []; @@ -1123,6 +2924,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Add an interface-language specific suggestion to all theme hooks. @@ -1137,12 +2939,13 @@ 'description' => 'Alters named suggestions for a specific theme hook.', 'destination' => '%module.module', 'has_return' => false, - 'procedural' => false, + 'procedural' => true, 'dependencies' => array ( ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' if (empty($variables[\'header\'])) { @@ -1164,6 +2967,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' foreach ($theme_list as $theme) { @@ -1185,6 +2989,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Remove some state entries depending on the theme. @@ -1207,6 +3012,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Extension for template base names in Twig. @@ -1227,6 +3033,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $twig_service = \\Drupal::service(\'twig\'); @@ -1248,6 +3055,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Decrease the default size of textfields. @@ -1270,6 +3078,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Use a custom class for the LayoutBuilder element. @@ -1290,6 +3099,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Swap out jQuery to use an updated version of the library. @@ -1310,6 +3120,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $libraries = []; @@ -1381,6 +3192,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Manipulate settings. @@ -1403,6 +3215,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Add settings. @@ -1428,6 +3241,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Update imaginary library \'foo\' to version 2.0. @@ -1474,6 +3288,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Remove defaults.css file. @@ -1495,6 +3310,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Unconditionally attach an asset to the page. @@ -1520,6 +3336,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Conditionally remove an asset. @@ -1543,6 +3360,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $page_top[\'my_module\'] = [\'#markup\' => \'This is the top.\']; @@ -1562,6 +3380,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $page_bottom[\'my_module\'] = [\'#markup\' => \'This is the bottom.\']; @@ -1581,6 +3400,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' return [ @@ -1614,6 +3434,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' // Kill the next/previous my_module topic navigation links. @@ -1638,6 +3459,7 @@ ), 'group' => 'core:theme', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Render/theme.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_theme.api.php', 'body' => ' $variables[\'is_admin\'] = \\Drupal::currentUser()->hasPermission(\'access administration pages\'); @@ -1660,6 +3482,7 @@ ), 'group' => 'core:token', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Utility/token.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_token.api.php', 'body' => ' $token_service = \\Drupal::token(); @@ -1732,6 +3555,7 @@ ), 'group' => 'core:token', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Utility/token.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_token.api.php', 'body' => ' if ($context[\'type\'] == \'node\' && !empty($context[\'data\'][\'node\'])) { @@ -1760,6 +3584,7 @@ ), 'group' => 'core:token', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Utility/token.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_token.api.php', 'body' => ' $type = [ @@ -1811,6 +3636,7 @@ ), 'group' => 'core:token', 'core' => true, + 'original_file_path' => 'core/lib/Drupal/Core/Utility/token.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/CORE_token.api.php', 'body' => ' // Modify description of node tokens for our site. @@ -1848,6 +3674,7 @@ ), 'group' => 'help', 'core' => true, + 'original_file_path' => 'core/modules/help/help.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/help.api.php', 'body' => ' switch ($route_name) { @@ -1875,6 +3702,7 @@ ), 'group' => 'help', 'core' => true, + 'original_file_path' => 'core/modules/help/help.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/help.api.php', 'body' => ' // Alter the header for the module overviews section. @@ -1897,6 +3725,7 @@ ), 'group' => 'help', 'core' => true, + 'original_file_path' => 'core/modules/help/help.api.php', 'file_path' => '/Users/joachim/Sites/dcb-repos-9/repos/drupal-code-builder/Test/sample_hook_definitions/11/help.api.php', 'body' => ' // Alter the help topic to be displayed on admin/help. diff --git a/Test/sample_hook_definitions/11/plugins_processed.php b/Test/sample_hook_definitions/11/plugins_processed.php index ce5087e2..92a7cc63 100644 --- a/Test/sample_hook_definitions/11/plugins_processed.php +++ b/Test/sample_hook_definitions/11/plugins_processed.php @@ -19,6 +19,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/lib/Drupal/Core/Block/BlockPluginInterface.php', 'base_class' => 'Drupal\\Core\\Block\\BlockBase', 'base_class_has_di' => false, 'config_schema_prefix' => 'block.settings.', @@ -168,6 +169,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/lib/Drupal/Core/Render/Element/ElementInterface.php', 'base_class' => 'Drupal\\Core\\Render\\Element\\RenderElementBase', 'base_class_has_di' => false, 'plugin_properties' => @@ -220,6 +222,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/lib/Drupal/Core/Field/FormatterInterface.php', 'base_class' => 'Drupal\\Core\\Field\\FormatterBase', 'base_class_has_di' => true, 'config_schema_prefix' => 'field.formatter.settings.', @@ -402,6 +405,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/modules/filter/src/Plugin/FilterInterface.php', 'base_class' => 'Drupal\\filter\\Plugin\\FilterBase', 'base_class_has_di' => false, 'config_schema_prefix' => 'filter_settings.', @@ -527,6 +531,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/modules/image/src/ImageEffectInterface.php', 'base_class' => 'Drupal\\image\\ImageEffectBase', 'base_class_has_di' => true, 'config_schema_prefix' => 'image.effect.', @@ -827,6 +832,7 @@ 'yaml_properties' => array ( ), + 'plugin_interface_filepath' => 'core/modules/views/src/Plugin/views/ViewsHandlerInterface.php', 'base_class' => 'Drupal\\views\\Plugin\\views\\area\\AreaPluginBase', 'base_class_has_di' => true, 'plugin_properties' => diff --git a/Test/sample_hook_definitions/11/service_tag_types_processed.php b/Test/sample_hook_definitions/11/service_tag_types_processed.php index d9af70be..bc8d0307 100644 --- a/Test/sample_hook_definitions/11/service_tag_types_processed.php +++ b/Test/sample_hook_definitions/11/service_tag_types_processed.php @@ -5,6 +5,7 @@ 'label' => 'Breadcrumb builder', 'collector_type' => 'service_collector', 'interface' => 'Drupal\\Core\\Breadcrumb\\BreadcrumbBuilderInterface', + 'interface_filepath' => 'core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php', 'methods' => array ( 'applies' => diff --git a/Test/sample_hook_definitions/11/services_processed.php b/Test/sample_hook_definitions/11/services_processed.php index d6ac5137..7b1496a1 100644 --- a/Test/sample_hook_definitions/11/services_processed.php +++ b/Test/sample_hook_definitions/11/services_processed.php @@ -32,6 +32,16 @@ ), 'all' => array ( + 'Drupal\\Core\\DefaultContent\\Importer' => + array ( + 'id' => 'Drupal\\Core\\DefaultContent\\Importer', + 'label' => 'Importer', + 'static_method' => '', + 'class' => '\\Drupal\\Core\\DefaultContent\\Importer', + 'interface' => '\\Psr\\Log\\LoggerAwareInterface', + 'description' => 'The importer service', + 'variable_name' => 'importer', + ), 'cache.discovery' => array ( 'id' => 'cache.discovery', diff --git a/Utility/ArrayOrder.php b/Utility/ArrayOrder.php new file mode 100644 index 00000000..05faa5ac --- /dev/null +++ b/Utility/ArrayOrder.php @@ -0,0 +1,103 @@ + $value]); + } + + + /** + * Moves an item to the end of an array, specifying by value. + * + * @param array &$array + * The array to change. + * @param mixed $value + * The value to move. + * + * @throws \InvalidArgumentException + * Throws an exception if the value is not in the array. + */ + public static function moveValueToEnd(array &$array, mixed $value): void { + $key = array_find($array, $value); + + if ($key === FALSE) { + throw new \InvalidArgumentException('Value not found in array.'); + } + + unset($array[$key]); + + $array[$key] = $value; + } + + /** + * Moves an item to the end of an array, specifying by key. + * + * @param array &$array + * The array to change. + * @param mixed $key + * The key to move. + * + * @throws \InvalidArgumentException + * Throws an exception if the key is not in the array. + */ + public static function moveKeyToEnd(array &$array, mixed $key): void { + if (!isset($array[$key])) { + throw new \InvalidArgumentException('Key not found in array.'); + } + + $value = $array[$key]; + + unset($array[$key]); + + $array[$key] = $value; + } + +} diff --git a/Utility/NestedArray.php b/Utility/NestedArray.php index 0dddc84f..f008a3bc 100644 --- a/Utility/NestedArray.php +++ b/Utility/NestedArray.php @@ -361,7 +361,7 @@ public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FA * @return array * The filtered array. */ - public static function filter(array $array, callable $callable = NULL) { + public static function filter(array $array, ?callable $callable = NULL) { $array = is_callable($callable) ? array_filter($array, $callable) : array_filter($array); foreach ($array as &$element) { if (is_array($element)) { diff --git a/composer.json b/composer.json index 4a070c70..dffbcf07 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,15 @@ "symfony/finder": "^4.0 || ^5 || ^6 || ^7" }, "require-dev": { - "phpunit/phpunit": "^9", - "drupal/coder": "^8.3", + "phpunit/phpunit": "^10", + "drupal/coder": "^9.0@alpha", "mikey179/vfsstream": "^1.6.11", - "squizlabs/php_codesniffer": "^3", + "nikic/php-parser": "^5.0", + "squizlabs/php_codesniffer": "^4.0", "symfony/yaml": "^6", "symfony/var-dumper": "^6", "morrislaptop/var-dumper-with-context": "^0.1.0", - "phpspec/prophecy-phpunit": "^2.0", - "dms/phpunit-arraysubset-asserts": "^0.3.0" + "phpspec/prophecy-phpunit": "^2.0" }, "autoload": { "psr-4": {