Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ All notable changes to the Pollora framework will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0...develop)
## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.1...develop)

## [v13.34.1](https://github.com/Pollora/framework/compare/v13.34.0...v13.34.1) - 2026-10-02

### Removed
- The `use_default_wp_theme_directory` key of `config/wordpress.php`. Nothing ever read it: setting it to `true` changed nothing, and themes always live in `themes/` (#297)

### Fixed
- Under WordPress 7, a classic theme's block styles and `global-styles` were printed at the bottom of every page, after the content they style — a flash of unstyled content on slow connections — and the head kept two empty placeholders. WordPress loads those styles on demand at `wp_footer`, then moves them back into the head through its template enhancement output buffer, which only starts when `template-loader.php` includes a template: a Blade page includes none. A `WordPressTemplateEnhancement` middleware now plays the buffer's part on the response — `wp_template_enhancement_output_buffer_started` before the view renders, the `wp_template_enhancement_output_buffer` filter and `wp_finalized_template_enhancement_output_buffer` on the HTML — for `Route::wp()` routes and the template hierarchy, so any plugin built on that filter sees Pollora's pages too. A plain Laravel route that prints `wp_head()`/`wp_footer()` can add the middleware itself
- `pollora:install` without a terminal — `--no-interaction`, CI, or `pollora new` driving it — stopped on "Site title is required" unless every option was passed: the prompts it could not show were still required. It now fills what is missing with a working local site — the project name as title (the first label of the `APP_URL` host: under DDEV the directory is always `html`), `admin` at `admin@<APP_URL host>`, a generated password shown once (also with `--install`), `en_US`, not indexed — and keeps every option it is given
- `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

Expand Down
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,29 @@
<a href="LICENSE"><img src="https://img.shields.io/github/license/Pollora/framework" alt="License"></a>
</p>

## About Pollora Framework
## About Pollora

Pollora is a framework that bridges **Laravel** and **WordPress**, combining Laravel's architecture patterns with WordPress's content management capabilities. It allows developers to use Laravel's service providers, dependency injection, Blade templates, and Eloquent ORM while maintaining full WordPress functionality.
**Pollora is the Laravel framework for WordPress.** WordPress runs inside a Laravel application: the front end uses Laravel routing, controllers, Blade and Eloquent, while the WordPress admin, database, editors' workflow and plugins keep working as usual. Hooks, post types, taxonomies and REST routes are declared with PHP 8 attributes and registered by auto-discovery.

[Website](https://pollora.dev) · [Documentation](https://pollora.dev/getting-started/installation/) · [Why Pollora](https://pollora.dev/why/) · [How Pollora compares with Acorn, Sage, Radicle and Corcel](https://pollora.dev/compare/) · [1-minute tour](https://www.youtube.com/watch?v=Wk1VzPapqM8)

```php
use Pollora\Attributes\Filter;
use Pollora\Attributes\PostType;
use Pollora\Attributes\PostType\HasArchive;
use Pollora\Attributes\PostType\Supports;

#[PostType]
#[HasArchive]
#[Supports(['title', 'editor', 'thumbnail'])]
class Book {}

class Seo
{
#[Filter('document_title_parts')]
public function title(array $parts): array { /* ... */ return $parts; }
}
```

### Key Features

Expand All @@ -32,19 +52,28 @@ Full documentation is available at **[pollora.dev](https://pollora.dev)**.

## Installation

Pollora is installed via the [skeleton project](https://github.com/Pollora/pollora) — see the [installation guide](https://pollora.dev/getting-started/installation/):
Create a project with the [Pollora CLI](https://github.com/Pollora/cli), or with Composer through the [skeleton](https://github.com/Pollora/pollora). See the [installation guide](https://pollora.dev/getting-started/installation/):

```bash
composer global require pollora/cli
pollora new my-project --ddev

# or
composer create-project pollora/pollora my-project
```

See the [skeleton README](https://github.com/Pollora/pollora) for detailed setup instructions.

## Requirements

- PHP ^8.3
- Laravel 13.x
- WordPress 6.9+
- PHP 8.4+ for a new project (the skeleton's lock file ships Symfony 8; this package alone accepts PHP 8.3)
- Laravel 13.34 (Pollora's version numbers follow the Laravel release it is built on)
- WordPress 7.1+

## Learn more

- [WordPress hooks with PHP 8 attributes](https://pollora.dev/guides/wordpress-hooks-php-attributes/)
- [Custom post types and taxonomies with PHP attributes](https://pollora.dev/guides/custom-post-types-php-attributes/)
- [How Pollora runs WordPress inside Laravel](https://pollora.dev/guides/how-pollora-runs-wordpress-inside-laravel/)
- [AI coding agents for WordPress projects (Nectar)](https://pollora.dev/guides/ai-coding-agents-wordpress/)

## Testing

Expand All @@ -68,6 +97,10 @@ If you discover a security vulnerability, please report it via [GitHub Security

All notable changes are documented in [CHANGELOG.md](CHANGELOG.md).

## Credits

Pollora builds on the work of [Roots](https://roots.io), a constant source of inspiration: themes, blocks and plugins are built with `@roots/vite-plugin`, the project follows the Bedrock layout with `roots/bedrock-autoloader`, and Blade ships [Sage Directives](https://github.com/Log1x/sage-directives). The WordPress models are built on [Colt](https://github.com/Pollora/colt), a fork of [Corcel](https://github.com/corcel/corcel).

## License

Pollora is open-sourced software licensed under the [MIT license](LICENSE).
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pollora/framework",
"description": "Laravel & WordPress blend for coding as sweet as honey. Join the hive!",
"description": "The Laravel framework for WordPress: Laravel routing, Blade and Eloquent, with PHP 8 attributes for hooks, post types, taxonomies and REST routes.",
"license": "MIT",
"homepage": "https://pollora.dev",
"support": {
Expand Down
9 changes: 0 additions & 9 deletions config/wordpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,6 @@
'is_subpage' => ['subpage', 'subpageof'],
],

/**
* Theme configuration.
*
* Controls whether Pollora framework should use the default WordPress theme directory.
* When enabled (true), Pollora will use the default WordPress theme directory.
* When disabled (false), Pollora will use the custom theme directory (./themes).
*/
'use_default_wp_theme_directory' => false,

/**
* Mail handling configuration.
*
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
Loading
Loading