From 60dabdf5a92b9dd2f0f8fa017b0a0bed9ec8316b Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 30 Sep 2026 15:31:37 +0200 Subject: [PATCH] fix: pollora:status reports the slug the attribute registers Closes #298. The slug rule moves to PostType::resolveSlug() and Taxonomy::resolveSlug(), used by discovery and by the status collector alike. --- CHANGELOG.md | 3 ++ src/Attributes/PostType.php | 15 +++++++ src/Attributes/Taxonomy.php | 14 ++++++ .../Domain/Services/SystemInfoCollector.php | 43 +++++++++++++++---- .../Services/PostTypeDiscovery.php | 20 +-------- .../Services/TaxonomyDiscovery.php | 20 +-------- .../Unit/Attributes/PostTypeAttributeTest.php | 10 +++++ .../Unit/Attributes/TaxonomyAttributeTest.php | 6 +++ .../Dashboard/SystemInfoCollectorTest.php | 20 +++++++++ 9 files changed, 105 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11678d7..a7e2b19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0...develop) +### Fixed +- `pollora:status` and the dashboard reported a post type or taxonomy under a slug derived from its class name, ignoring the attribute: `#[PostType('synthese-presse')] class SyntheseDePresse` was listed as `synthese-de-presse`, a post type that does not exist, and its `plural` label was ignored too. Both read the attribute now, through `PostType::resolveSlug()` / `Taxonomy::resolveSlug()`, the rule discovery registers with, so they cannot disagree again (#298) + ## [v13.34.0](https://github.com/Pollora/framework/compare/v13.34.0-beta.2...v13.34.0) - 2026-09-30 The first stable release since v13.4.4, on Laravel 13.34. It gathers every change from v13.32.0-beta to v13.34.0-beta.2 below; coming from 13.4, read those entries, or run Nectar's `upgrade-pollora-v13-32` prompt ([full comparison](https://github.com/Pollora/framework/compare/v13.4.4...v13.34.0)). diff --git a/src/Attributes/PostType.php b/src/Attributes/PostType.php index 17cc4d9d..80ba791f 100644 --- a/src/Attributes/PostType.php +++ b/src/Attributes/PostType.php @@ -5,6 +5,7 @@ namespace Pollora\Attributes; use Attribute; +use Illuminate\Support\Str; /** * PostType Attribute @@ -38,4 +39,18 @@ public function __construct( ) { // No validation here - will be handled by PostTypeDiscovery } + + /** + * The slug WordPress registers the post type under. + * + * The explicit slug when the attribute has one; otherwise the class name in + * kebab-case, cut to 20 characters. Discovery registers with it and + * `pollora:status` reports it, so the two cannot disagree. + * + * @param string $className The class carrying the attribute + */ + public function resolveSlug(string $className): string + { + return $this->slug ?? substr(Str::kebab(class_basename($className)), 0, 20); + } } diff --git a/src/Attributes/Taxonomy.php b/src/Attributes/Taxonomy.php index ee62c6fe..c301d052 100644 --- a/src/Attributes/Taxonomy.php +++ b/src/Attributes/Taxonomy.php @@ -5,6 +5,7 @@ namespace Pollora\Attributes; use Attribute; +use Illuminate\Support\Str; /** * Attribute for defining custom taxonomies. @@ -49,4 +50,17 @@ public function __construct( public ?string $textDomain = null ) {} + /** + * The slug WordPress registers the taxonomy under. + * + * The explicit slug when the attribute has one; otherwise the class name in + * kebab-case, cut to 20 characters. Discovery registers with it and + * `pollora:status` reports it, so the two cannot disagree. + * + * @param string $className The class carrying the attribute + */ + public function resolveSlug(string $className): string + { + return $this->slug ?? substr(Str::kebab(class_basename($className)), 0, 20); + } } diff --git a/src/Dashboard/Domain/Services/SystemInfoCollector.php b/src/Dashboard/Domain/Services/SystemInfoCollector.php index f23775ac..5d8ae3c6 100644 --- a/src/Dashboard/Domain/Services/SystemInfoCollector.php +++ b/src/Dashboard/Domain/Services/SystemInfoCollector.php @@ -5,6 +5,8 @@ namespace Pollora\Dashboard\Domain\Services; use Nwidart\Modules\Contracts\RepositoryInterface; +use Pollora\Attributes\PostType; +use Pollora\Attributes\Taxonomy; use Pollora\Discovery\Application\Services\DiscoveryManager; use Pollora\Support\Domain\StringHelper; use Pollora\VersionCheck\Domain\Services\VersionComparator; @@ -212,8 +214,9 @@ private function collectPostTypeInfo(): array } $class = $item['class']; - $slug = StringHelper::kebab(class_basename($class)); - $label = $this->getPostTypeLabel($slug, $class); + $attribute = $this->attributeOf($class, PostType::class); + $slug = $attribute?->resolveSlug($class) ?? StringHelper::kebab(class_basename($class)); + $label = $this->getPostTypeLabel($slug, $class, $attribute?->plural); $result[] = [ 'class' => $class, @@ -243,8 +246,9 @@ private function collectTaxonomyInfo(): array } $class = $item['class']; - $slug = StringHelper::kebab(class_basename($class)); - $label = $this->getTaxonomyLabel($slug, $class); + $attribute = $this->attributeOf($class, Taxonomy::class); + $slug = $attribute?->resolveSlug($class) ?? StringHelper::kebab(class_basename($class)); + $label = $this->getTaxonomyLabel($slug, $class, $attribute?->plural); $result[] = [ 'class' => $class, @@ -259,7 +263,30 @@ private function collectTaxonomyInfo(): array } } - private function getPostTypeLabel(string $slug, string $class): string + /** + * The registration attribute a discovered class carries, when it can be read. + * + * Its slug is the one WordPress registered, which the class name alone + * does not give: `#[PostType('synthese-presse')] class SyntheseDePresse` + * is `synthese-presse`, not `synthese-de-presse`. + * + * @template T of object + * + * @param class-string $attributeClass + * @return T|null + */ + private function attributeOf(string $class, string $attributeClass): ?object + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new \ReflectionClass($class))->getAttributes($attributeClass); + + return $attributes === [] ? null : $attributes[0]->newInstance(); + } + + private function getPostTypeLabel(string $slug, string $class, ?string $plural = null): string { if (function_exists('get_post_type_object')) { $object = get_post_type_object($slug); @@ -269,10 +296,10 @@ private function getPostTypeLabel(string $slug, string $class): string } } - return StringHelper::headline(class_basename($class)); + return $plural ?? StringHelper::headline(class_basename($class)); } - private function getTaxonomyLabel(string $slug, string $class): string + private function getTaxonomyLabel(string $slug, string $class, ?string $plural = null): string { if (function_exists('get_taxonomy')) { $object = get_taxonomy($slug); @@ -282,7 +309,7 @@ private function getTaxonomyLabel(string $slug, string $class): string } } - return StringHelper::headline(class_basename($class)); + return $plural ?? StringHelper::headline(class_basename($class)); } /** diff --git a/src/PostType/Infrastructure/Services/PostTypeDiscovery.php b/src/PostType/Infrastructure/Services/PostTypeDiscovery.php index 2e5fbee1..0095dd91 100644 --- a/src/PostType/Infrastructure/Services/PostTypeDiscovery.php +++ b/src/PostType/Infrastructure/Services/PostTypeDiscovery.php @@ -222,7 +222,7 @@ private function processPostType(string $className, ?ReflectionCacheInterface $r */ private function buildBaseConfiguration(string $className, PostType $postType): PostTypeConfiguration { - $slug = $this->generateSlug($className, $postType->slug); + $slug = $postType->resolveSlug($className); $singular = $this->generateSingular($className, $postType->singular); $plural = $this->generatePlural($postType->plural, $singular); $textDomain = $postType->textDomain ?? 'pollora'; @@ -365,24 +365,6 @@ private function processAdditionalArgs(string $className, PostTypeConfiguration } } - /** - * Generate a post type slug from class name and attribute value. - * - * @param string $className The class name - * @param string|null $attributeSlug The slug from the attribute - * @return string The generated slug - */ - private function generateSlug(string $className, ?string $attributeSlug): string - { - if ($attributeSlug !== null) { - return $attributeSlug; - } - - $slug = Str::kebab(class_basename($className)); - - return substr($slug, 0, 20); - } - /** * Generate a singular name from class name and attribute value. * diff --git a/src/Taxonomy/Infrastructure/Services/TaxonomyDiscovery.php b/src/Taxonomy/Infrastructure/Services/TaxonomyDiscovery.php index fdca1dc5..2cb6ca82 100644 --- a/src/Taxonomy/Infrastructure/Services/TaxonomyDiscovery.php +++ b/src/Taxonomy/Infrastructure/Services/TaxonomyDiscovery.php @@ -226,7 +226,7 @@ private function processTaxonomy(string $className, ?ReflectionCacheInterface $r */ private function buildBaseConfiguration(string $className, Taxonomy $taxonomy): TaxonomyConfiguration { - $slug = $this->generateSlug($className, $taxonomy->slug); + $slug = $taxonomy->resolveSlug($className); $singular = $this->generateSingular($className, $taxonomy->singular); $plural = $this->generatePlural($taxonomy->plural, $singular); $objectType = $taxonomy->objectType ?? ['post']; @@ -370,24 +370,6 @@ private function processAdditionalArgs(string $className, TaxonomyConfiguration } } - /** - * Generate a taxonomy slug from class name and attribute value. - * - * @param string $className The class name - * @param string|null $attributeSlug The slug from the attribute - * @return string The generated slug - */ - private function generateSlug(string $className, ?string $attributeSlug): string - { - if ($attributeSlug !== null) { - return $attributeSlug; - } - - $slug = Str::kebab(class_basename($className)); - - return substr($slug, 0, 20); - } - /** * Generate a singular name from class name and attribute value. * diff --git a/tests/Unit/Attributes/PostTypeAttributeTest.php b/tests/Unit/Attributes/PostTypeAttributeTest.php index 7be03b79..2c0cd6b9 100644 --- a/tests/Unit/Attributes/PostTypeAttributeTest.php +++ b/tests/Unit/Attributes/PostTypeAttributeTest.php @@ -332,3 +332,13 @@ public function getProductDetails(): array // AdminCol with any values expect(fn (): AdminCol => new AdminCol('', ''))->not->toThrow(Exception::class); }); + +test('resolveSlug returns the explicit slug', function (): void { + expect((new PostType('synthese-presse'))->resolveSlug('App\\Cms\\PostTypes\\SyntheseDePresse')) + ->toBe('synthese-presse'); +}); + +test('resolveSlug derives the slug from the class name, cut to 20 characters', function (): void { + expect((new PostType)->resolveSlug('App\\Cms\\PostTypes\\SyntheseDePresse'))->toBe('synthese-de-presse') + ->and((new PostType)->resolveSlug('App\\VeryLongPostTypeClassName'))->toBe('very-long-post-type-'); +}); diff --git a/tests/Unit/Attributes/TaxonomyAttributeTest.php b/tests/Unit/Attributes/TaxonomyAttributeTest.php index f06c627a..66edb2fb 100644 --- a/tests/Unit/Attributes/TaxonomyAttributeTest.php +++ b/tests/Unit/Attributes/TaxonomyAttributeTest.php @@ -5,6 +5,7 @@ use Illuminate\Container\Container; use Illuminate\Support\Facades\Facade; use Mockery as m; +use Pollora\Attributes\Taxonomy; use Pollora\Attributes\Taxonomy\AllowHierarchy; use Pollora\Attributes\Taxonomy\Args; use Pollora\Attributes\Taxonomy\CheckedOntop; @@ -285,3 +286,8 @@ function testMethodAttribute(string $attributeName, string $argName, string $met ->toHaveKey('name') ->toHaveKey('singular_name'); }); + +test('resolveSlug returns the explicit slug, or the class name in kebab-case', function (): void { + expect((new Taxonomy('theme-presse'))->resolveSlug('App\\Cms\\Taxonomies\\ThemeDePresse'))->toBe('theme-presse') + ->and((new Taxonomy)->resolveSlug('App\\Cms\\Taxonomies\\ThemeDePresse'))->toBe('theme-de-presse'); +}); diff --git a/tests/Unit/Dashboard/SystemInfoCollectorTest.php b/tests/Unit/Dashboard/SystemInfoCollectorTest.php index 0a5b8bdc..d499d1e8 100644 --- a/tests/Unit/Dashboard/SystemInfoCollectorTest.php +++ b/tests/Unit/Dashboard/SystemInfoCollectorTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Pollora\Attributes\PostType; +use Pollora\Attributes\Taxonomy; use Pollora\Dashboard\Domain\Services\SystemInfoCollector; use Pollora\Discovery\Application\Services\DiscoveryManager; use Pollora\Discovery\Domain\Contracts\DiscoveryEngineInterface; @@ -14,6 +16,12 @@ use Spatie\StructureDiscoverer\Cache\LaravelDiscoverCacheDriver; use Spatie\StructureDiscoverer\Cache\NullDiscoverCacheDriver; +#[PostType('synthese-presse', singular: 'Synthèse de presse', plural: 'Synthèses de presse')] +class SyntheseDePresse {} + +#[Taxonomy('theme-presse', singular: 'Thème de presse', plural: 'Thèmes de presse')] +class ThemeDePresse {} + function createCollector( ?VersionComparator $comparator = null, ?DiscoveryManager $manager = null, @@ -140,6 +148,18 @@ function createCollector( expect($info['post_types']['items'][0]['slug'])->toBe('project'); }); + it('reports the slug and labels of the #[PostType] attribute, not the class name', function (): void { + $manager = Mockery::mock(DiscoveryManager::class); + $manager->shouldReceive('getDiscoveredItems')->with('post_types')->andReturn([['class' => SyntheseDePresse::class]]); + $manager->shouldReceive('getDiscoveredItems')->with('taxonomies')->andReturn([['class' => ThemeDePresse::class]]); + $manager->shouldReceive('getDiscoveredItems')->withAnyArgs()->andReturn([]); + + $info = createCollector(manager: $manager)->collectDiscoveryInfo(); + + expect($info['post_types']['items'][0])->toMatchArray(['slug' => 'synthese-presse', 'label' => 'Synthèses de presse']) + ->and($info['taxonomies']['items'][0])->toMatchArray(['slug' => 'theme-presse', 'label' => 'Thèmes de presse']); + }); + it('collects hook counts by type', function (): void { $manager = Mockery::mock(DiscoveryManager::class); $manager->shouldReceive('getDiscoveredItems')