Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
15 changes: 15 additions & 0 deletions src/Attributes/PostType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pollora\Attributes;

use Attribute;
use Illuminate\Support\Str;

/**
* PostType Attribute
Expand Down Expand Up @@ -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);
}
}
14 changes: 14 additions & 0 deletions src/Attributes/Taxonomy.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pollora\Attributes;

use Attribute;
use Illuminate\Support\Str;

/**
* Attribute for defining custom taxonomies.
Expand Down Expand Up @@ -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);
}
}
43 changes: 35 additions & 8 deletions src/Dashboard/Domain/Services/SystemInfoCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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<T> $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);
Expand All @@ -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);
Expand All @@ -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));
}

/**
Expand Down
20 changes: 1 addition & 19 deletions src/PostType/Infrastructure/Services/PostTypeDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
*
Expand Down
20 changes: 1 addition & 19 deletions src/Taxonomy/Infrastructure/Services/TaxonomyDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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.
*
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/Attributes/PostTypeAttributeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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-');
});
6 changes: 6 additions & 0 deletions tests/Unit/Attributes/TaxonomyAttributeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
});
20 changes: 20 additions & 0 deletions tests/Unit/Dashboard/SystemInfoCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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')
Expand Down
Loading