{"id":51611133,"url":"https://github.com/yaml-ns/wpp-framework","last_synced_at":"2026-07-12T08:01:01.713Z","repository":{"id":357283117,"uuid":"1236235281","full_name":"yaml-ns/WPP-Framework","owner":"yaml-ns","description":"Composer library for building application-style WordPress plugins — DI container, REST API, admin CRUD, CLI generators and more.","archived":false,"fork":false,"pushed_at":"2026-05-12T06:07:21.000Z","size":107,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-12T06:32:58.001Z","etag":null,"topics":["composer","framework","php","wordpress","wordpress-development","wordpress-plugin"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yaml-ns.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-12T04:18:52.000Z","updated_at":"2026-05-12T06:07:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yaml-ns/WPP-Framework","commit_stats":null,"previous_names":["yaml-ns/wpp-framework"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/yaml-ns/WPP-Framework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaml-ns%2FWPP-Framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaml-ns%2FWPP-Framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaml-ns%2FWPP-Framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaml-ns%2FWPP-Framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yaml-ns","download_url":"https://codeload.github.com/yaml-ns/WPP-Framework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaml-ns%2FWPP-Framework/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35385772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-12T02:00:06.386Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composer","framework","php","wordpress","wordpress-development","wordpress-plugin"],"created_at":"2026-07-12T08:00:55.112Z","updated_at":"2026-07-12T08:01:01.699Z","avatar_url":"https://github.com/yaml-ns.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WPP Framework\n\n[![CI](https://github.com/yaml-ns/WPP-Framework/actions/workflows/tests.yml/badge.svg)](https://github.com/yaml-ns/WPP-Framework/actions/workflows/tests.yml)\n\nReusable PHP framework for building application-style WordPress plugins: REST APIs, admin screens, business services, assets, cron, permissions, modules and PHP views.\n\nLicense: MIT.\n\nNamespace:\n\n```php\nYamlNs\\WppFramework\n```\n\n## Goal\n\nWPP Framework is a shared Composer library installed inside each consumer plugin. It avoids rewriting the same infrastructure for every plugin:\n\n- plugin bootstrap;\n- dependency injection container;\n- service providers;\n- REST routes and middleware;\n- security helpers;\n- custom post types and taxonomies;\n- metaboxes and meta fields;\n- admin pages and admin forms;\n- admin CRUD screens;\n- asset enqueueing;\n- WordPress settings;\n- cron jobs;\n- AJAX actions;\n- modules;\n- PHP view rendering.\n\nThe framework supports multiple active plugins in the same WordPress installation. Each plugin gets its own `PluginContext`, container and provider list.\n\n## Installation\n\nInside a consumer plugin:\n\n```json\n{\n  \"require\": {\n    \"yaml-ns/wpp-framework\": \"^0.9\"\n  }\n}\n```\n\nThen run:\n\n```bash\ncomposer install\n```\n\nThe framework package is a Composer `library`, not a `wordpress-plugin`.\n\n## Public Bootstrap\n\nThe main file of a consumer plugin can stay small:\n\n```php\n\u003c?php\n/**\n * Plugin Name: My Plugin\n */\n\ndeclare(strict_types=1);\n\nuse YamlNs\\WppFramework\\Wpp;\n\nif (!defined('ABSPATH')) {\n    exit;\n}\n\n$pluginFile = __FILE__;\n$pluginDir = plugin_dir_path($pluginFile);\n\nrequire_once $pluginDir . 'vendor/autoload.php';\n\n$config = require $pluginDir . 'config/plugin.php';\n\nregister_activation_hook($pluginFile, static function () use ($pluginFile, $config): void {\n    Wpp::activate($pluginFile, $config);\n});\n\nadd_action('plugins_loaded', static function () use ($pluginFile, $config): void {\n    Wpp::boot($pluginFile, $config);\n});\n\nregister_deactivation_hook($pluginFile, static function () use ($pluginFile, $config): void {\n    Wpp::deactivate($pluginFile, $config);\n});\n```\n\n`Wpp::activate()` registers declarative structures that affect rewrite rules, such as custom post types, runs lifecycle migrations, then flushes rewrite rules.\n\n`Wpp::boot()` creates the `PluginContext`, retrieves the isolated framework instance for this plugin, registers configured providers, then boots the plugin.\n\n`Wpp::deactivate()` clears declared cron jobs and flushes rewrite rules.\n\nFor full cleanup when the plugin is deleted, add an `uninstall.php` file in the consumer plugin:\n\n```php\n\u003c?php\ndeclare(strict_types=1);\n\nuse YamlNs\\WppFramework\\Wpp;\n\nif (!defined('WP_UNINSTALL_PLUGIN')) {\n    exit;\n}\n\n$pluginFile = __DIR__ . '/my-plugin.php';\n\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nWpp::uninstall($pluginFile, require __DIR__ . '/config/plugin.php');\n```\n\n## Configuration\n\nExample `config/plugin.php`:\n\n```php\n\u003c?php\ndeclare(strict_types=1);\n\nuse MyPlugin\\PostTypes\\BookPostType;\nuse MyPlugin\\Shortcodes\\BookListShortcode;\nuse MyPlugin\\Taxonomies\\BookGenreTaxonomy;\nuse YamlNs\\WppFramework\\Core\\Container;\n\nreturn [\n    'slug' =\u003e 'my-plugin',\n    'name' =\u003e 'My Plugin',\n    'version' =\u003e '1.0.0',\n    'text_domain' =\u003e 'my-plugin',\n    'rest_namespace' =\u003e 'my-plugin/v1',\n    'i18n' =\u003e [\n        'path' =\u003e 'languages',\n    ],\n    'post_types' =\u003e [\n        BookPostType::class,\n    ],\n    'taxonomies' =\u003e [\n        BookGenreTaxonomy::class,\n    ],\n    'admin' =\u003e require __DIR__ . '/admin.php',\n    'admin_forms' =\u003e require __DIR__ . '/admin-forms.php',\n    'admin_crud' =\u003e require __DIR__ . '/admin-crud.php',\n    'meta_boxes' =\u003e require __DIR__ . '/meta-boxes.php',\n    'routes' =\u003e [\n        __DIR__ . '/../routes/api.php',\n    ],\n    'shortcodes' =\u003e [\n        'book_list' =\u003e [BookListShortcode::class, 'render'],\n    ],\n    'assets' =\u003e require __DIR__ . '/assets.php',\n    'lifecycle' =\u003e [\n        'option' =\u003e 'my_plugin_version',\n        'migrations' =\u003e [\n            '1.0.0' =\u003e static function (Container $container): void {\n                // Initial migration.\n            },\n        ],\n    ],\n    'logger' =\u003e [\n        'enabled' =\u003e true,\n        'min_level' =\u003e 'warning',\n    ],\n    'uninstall' =\u003e [\n        'options' =\u003e [\n            'my_plugin_version',\n            'my_plugin_settings',\n        ],\n        'remove_capabilities' =\u003e true,\n    ],\n    'providers' =\u003e [\n        // Custom providers only when the plugin needs specific behavior.\n    ],\n];\n```\n\nEach plugin must explicitly declare a unique `slug` and `rest_namespace`. `Wpp::boot()` and `Wpp::activate()` reject application configs that do not provide them.\n\nFor standard needs, `Wpp::boot()` can read `post_types`, `taxonomies`, `meta_boxes`, `shortcodes`, `assets`, `admin`, `admin_forms`, `admin_crud`, `routes`, `rest_controllers`, `settings`, `cron`, `ajax`, `i18n`, `capabilities`, `lifecycle`, `logger` and `uninstall` directly from config.\n\nThe config is validated before boot and activation so common mistakes fail early.\n\n## Recommended Plugin Structure\n\n```txt\nmy-plugin/\n  my-plugin.php\n  composer.json\n  app/\n    Admin/\n    Http/\n      Controllers/\n      Requests/\n    Policies/\n    PostTypes/\n    Providers/\n    Repositories/\n    Services/\n    Shortcodes/\n    Taxonomies/\n  assets/\n    admin.css\n    admin.js\n  config/\n    plugin.php\n    admin.php\n    admin-crud.php\n    assets.php\n    cron.php\n    settings.php\n  routes/\n    api.php\n  resources/\n    views/\n      admin/\n      shortcodes/\n  vendor/\n```\n\nThe framework stays in `vendor/`. The plugin's application code stays in `app/`.\n\n## Create A Plugin From Scratch\n\nMinimal flow for a `product-catalog` plugin:\n\n1. Create the WordPress plugin and install the framework:\n\n```bash\ncomposer require yaml-ns/wpp-framework\n```\n\n2. Add the main `product-catalog.php` file with `Wpp::activate()`, `Wpp::boot()` and `Wpp::deactivate()`.\n\n3. Create `config/plugin.php` with the plugin identity:\n\n```php\nreturn [\n    'slug' =\u003e 'product-catalog',\n    'name' =\u003e 'Product Catalog',\n    'version' =\u003e '1.0.0',\n    'rest_namespace' =\u003e 'products/v1',\n    'post_types' =\u003e [\n        ProductPostType::class,\n    ],\n    'routes' =\u003e [\n        __DIR__ . '/../routes/products.php',\n    ],\n    'admin_crud' =\u003e require __DIR__ . '/admin-crud.php',\n];\n```\n\n4. Generate the common pieces:\n\n```bash\nvendor/bin/wpp make:crud Product --plugin=/path/to/product-catalog --namespace=ProductCatalog\nvendor/bin/wpp make:admin-crud Product --plugin=/path/to/product-catalog --namespace=ProductCatalog\n```\n\n5. Adapt fields in the repository, requests and `config/admin-crud.php`.\n\n6. Add a view or shortcode when the plugin needs frontend output.\n\nThe `examples/product-catalog-plugin/` directory shows this flow with a `product` CPT, taxonomy, meta fields, settings, admin CRUD, REST API and shortcode.\n\n## Architecture Guidelines\n\nUse config for stable declarative parts: CPTs, taxonomies, assets, routes, metaboxes, admin CRUD, cron and settings.\n\nUse `routes/*.php` to connect REST URLs to controllers. Controllers should handle HTTP use cases, not route registration.\n\nUse a repository to isolate WordPress access (`WP_Query`, post meta, terms). Controllers, shortcodes and admin CRUD screens can then share the same data access logic.\n\nUse a `FormRequest` when a REST or admin action needs explicit validation. Use a `Policy` when authorization depends on the action or resource.\n\nCreate a custom `ServiceProvider` only when you need to register business services, wire an external integration, add container bindings or boot behavior that does not belong in declarative config.\n\n## Views\n\nPlugin PHP views live in `resources/views/` and can be rendered with `ViewRenderer`:\n\n```php\nfinal class BookListShortcode extends BaseShortcode\n{\n    public function __construct(ViewRenderer $viewRenderer)\n    {\n        parent::__construct($viewRenderer);\n    }\n\n    public function render(): string\n    {\n        return $this-\u003eview('shortcodes/book-list', [\n            'books' =\u003e [],\n        ]);\n    }\n}\n```\n\nView data is extracted with `extract(..., EXTR_SKIP)`, so a view receives `$books`, `$query`, `$context`, etc. directly. The `context` variable is reserved by the framework.\n\nThe view renderer is intentionally native PHP, but it supports layouts, includes, sections and stacks:\n\n```php\n\u003c?php $view-\u003eextends('layouts/admin'); ?\u003e\n\n\u003c?php $view-\u003esection('content'); ?\u003e\n    \u003ch1\u003e\u003c?php echo esc_html($title); ?\u003e\u003c/h1\u003e\n    \u003c?php $view-\u003einclude('partials/table', ['items' =\u003e $items]); ?\u003e\n\u003c?php $view-\u003eendSection(); ?\u003e\n\n\u003c?php $view-\u003epush('scripts'); ?\u003e\n    \u003cscript\u003ewindow.myPluginReady = true;\u003c/script\u003e\n\u003c?php $view-\u003eendPush(); ?\u003e\n```\n\nIn `resources/views/layouts/admin.php`:\n\n```php\n\u003cdiv class=\"wrap\"\u003e\n    \u003c?php echo $view-\u003eyield('content'); ?\u003e\n\u003c/div\u003e\n\n\u003c?php echo $view-\u003estack('scripts'); ?\u003e\n```\n\n## Container\n\nThe container centralizes plugin services: repositories, external API clients, import services, policies and similar application objects. For simple plugins, automatic autowiring is enough. Create a custom provider when a specific implementation must be configured.\n\n```php\nuse Psr\\Log\\LoggerInterface;\nuse YamlNs\\WppFramework\\Providers\\ServiceProvider;\n\nfinal class AppServiceProvider extends ServiceProvider\n{\n    public function register(): void\n    {\n        $this-\u003econtainer-\u003esingleton(ProductImporter::class);\n        $this-\u003econtainer-\u003etag([CsvImporter::class, ApiImporter::class], 'importers');\n\n        $this-\u003econtainer\n            -\u003ewhen(ProductSyncService::class)\n            -\u003eneeds(LoggerInterface::class)\n            -\u003egive(SyncLogger::class);\n    }\n}\n```\n\n`tagged('importers')` returns all tagged implementations. Contextual bindings are useful when two services need the same interface but different implementations.\n\n## Modules\n\nModules can be enabled from config:\n\n```php\n'modules' =\u003e [\n    [\n        'enabled' =\u003e true,\n        'providers' =\u003e [\n            BillingModuleProvider::class,\n            ReportingModuleProvider::class,\n        ],\n    ],\n],\n```\n\nA module is simply a group of providers. This keeps the core small and lets each plugin choose its own boundaries.\n\n## Included Providers\n\n### `AdminPageServiceProvider`\n\nRegisters admin pages from config:\n\n```php\nreturn [\n    'pages' =\u003e [\n        [\n            'menu_title' =\u003e 'My Plugin',\n            'page_title' =\u003e 'My Plugin',\n            'capability' =\u003e 'manage_options',\n            'slug' =\u003e 'my-plugin',\n            'template' =\u003e 'resources/views/admin/dashboard.php',\n            'icon' =\u003e 'dashicons-admin-generic',\n        ],\n    ],\n];\n```\n\n`AdminPageServiceProvider` is the declarative API for consumer plugin admin pages. `AdminServiceProvider` is only the internal/debug framework admin page.\n\n### `AdminFormServiceProvider`\n\nHandles admin forms through `admin-post.php`, checks capability, validates nonce, sanitizes fields and stores an option when no custom handler is provided.\n\n### `AssetServiceProvider`\n\nEnqueues admin and frontend assets. Admin assets can be limited to specific screens with `only`, and scripts can receive data with `localize`.\n\n### `LifecycleServiceProvider`\n\nStores the installed version and runs pending migrations during `Wpp::activate()`, not during every request.\n\n### `Logger`\n\nThe framework depends on `psr/log` and binds `Psr\\Log\\LoggerInterface` to its default `Logger`. A consumer plugin can replace this binding in a custom provider, including with Monolog.\n\n### `BaseRepository`\n\n`BaseRepository` provides a base for CPT repositories:\n\n```php\nfinal class BookRepository extends BaseRepository\n{\n    public function __construct(private readonly BookPostType $postType) {}\n\n    protected function postType(): string\n    {\n        return $this-\u003epostType-\u003ename();\n    }\n\n    protected function metaFields(): array\n    {\n        return [\n            'price' =\u003e '_book_price',\n            'isbn',\n        ];\n    }\n}\n```\n\nAvailable methods: `query()`, `all()`, `latest()`, `paginate()`, `where()`, `find()`, `findAny()`, `findOrFail()`, `create()`, `update()`, `delete()`, `toArray()`.\n\n`create()` and `update()` accept aliases such as `title`, `content`, `excerpt`, `status` and `slug`, plus WordPress fields declared by `writableFields()`. Meta and taxonomy values are synchronized only when declared in `metaFields()` / `taxonomyFields()`.\n\n### `MetaBoxServiceProvider`\n\nDeclares metaboxes and meta fields from config. The provider handles nonce, `register_post_meta()`, simple HTML rendering and saving.\n\nCheckbox fields are stored as postmeta strings `'1'` / `'0'`. Their default REST type is therefore `string`. If you need a real REST boolean, set `meta_type` and provide suitable sanitization.\n\nField sanitization goes through `FieldSanitizer`, shared by metaboxes and admin CRUD, to keep `number`, `integer`, `float`, `select_multiple` and `checkboxes` behavior consistent.\n\n### `AdminCrudServiceProvider`\n\nDeclares a simple admin CRUD interface for a CPT repository. The provider adds an admin page, renders `index` / `form` views, checks capability and nonce, then calls `create()`, `update()` and `delete()` on the repository.\n\n```php\n'admin_crud' =\u003e [\n    'resources' =\u003e [\n        [\n            'slug' =\u003e 'books',\n            'label' =\u003e 'Books',\n            'capability' =\u003e 'manage_books',\n            'force_delete' =\u003e false,\n            'repository' =\u003e BookRepository::class,\n            'policy' =\u003e BookPolicy::class,\n            'views' =\u003e [\n                'index' =\u003e 'admin/books/index',\n                'form' =\u003e 'admin/books/form',\n            ],\n            'filters' =\u003e [\n                's' =\u003e ['label' =\u003e 'Search', 'query' =\u003e 'search', 'type' =\u003e 'search'],\n                'status' =\u003e [\n                    'label' =\u003e 'Status',\n                    'query' =\u003e 'post_status',\n                    'type' =\u003e 'select',\n                    'options' =\u003e ['publish' =\u003e 'Published', 'draft' =\u003e 'Draft'],\n                ],\n                'featured' =\u003e [\n                    'label' =\u003e 'Featured',\n                    'meta_key' =\u003e '_book_featured',\n                    'type' =\u003e 'select',\n                    'options' =\u003e ['1' =\u003e 'Yes', '0' =\u003e 'No'],\n                ],\n            ],\n            'fields' =\u003e [\n                'title' =\u003e ['label' =\u003e 'Title', 'type' =\u003e 'text'],\n                'content' =\u003e ['label' =\u003e 'Content', 'type' =\u003e 'textarea'],\n                'price' =\u003e ['label' =\u003e 'Price', 'type' =\u003e 'float', 'meta_key' =\u003e '_book_price'],\n            ],\n            'rules' =\u003e [\n                'title' =\u003e ['required', 'string', 'max:120'],\n                'price' =\u003e ['nullable', 'numeric', 'min:0'],\n            ],\n        ],\n    ],\n],\n```\n\nAdmin-post actions are derived from the slug: `books_store`, `books_update`, `books_delete`, `books_bulk`.\n\nIndex views receive `$filters`, `$activeFilters`, `$currentPage` and `$totalPages`, so they can build search, filters, pagination and bulk actions.\n\nWhen admin validation fails, the provider redirects back to the form with `message=validation_failed`, stores errors and old input in a short transient, then injects `$errors` and `$values` into the view.\n\n### `ShortcodeServiceProvider`\n\nDeclares shortcodes and lets the container resolve class callbacks:\n\n```php\nreturn [\n    'shortcodes' =\u003e [\n        'product_catalog' =\u003e [ProductCatalogShortcode::class, 'render'],\n    ],\n];\n```\n\nShortcode tags must match `a-z`, `0-9`, `_` and `-`.\n\n## REST\n\nThe framework registers a public health endpoint:\n\n```txt\n/wp-json/{rest_namespace}/health\n```\n\nRecommended REST route declaration:\n\n```php\n\u003c?php\ndeclare(strict_types=1);\n\nuse MyPlugin\\Http\\Controllers\\BookRestController;\nuse YamlNs\\WppFramework\\Core\\PluginContext;\nuse YamlNs\\WppFramework\\Http\\RestRouter;\n\nreturn static function (RestRouter $router, PluginContext $context): void {\n    $router-\u003eget($context-\u003erestNamespace(), '/books', [BookRestController::class, 'index']);\n    $router-\u003eget($context-\u003erestNamespace(), '/books/(?P\u003cid\u003e\\d+)', [BookRestController::class, 'show']);\n};\n```\n\nThen register the file in `config/plugin.php`:\n\n```php\n'routes' =\u003e [\n    __DIR__ . '/../routes/api.php',\n],\n```\n\nThe controller stays focused on HTTP use cases and does not need to know `RestRouter`.\n\n`BaseRestController` provides `paginated()`, `notFound()`, `forbidden()`, `deleted()` and `handle()`. `handle()` converts a `ValidationException` into a `WP_Error` `wpp_validation_failed` with status `422`, and other exceptions into `400` errors.\n\n### Form Requests\n\nREST controllers can type a request class that extends `FormRequest`. The container builds it from `WP_REST_Request`, and `validated()` throws `ValidationException` when data is invalid.\n\nAvailable rules: `required`, `nullable`, `sometimes`, `required_if`, `required_with`, `string`, `numeric`, `integer`, `email`, `boolean`, `array`, `min`, `max`, `size`, `in`, `not_in`, `same`, `different`, `confirmed`, `url`, `date`, `alpha_dash`, `slug`, `json`, `regex`, `exists:post,post_type`, `exists:term,taxonomy`, `exists:user`.\n\nThe validator also accepts custom injected rules:\n\n```php\n$validator = new Validator([\n    'sku' =\u003e static fn (mixed $value): bool =\u003e preg_match('/^[A-Z0-9-]+$/', (string) $value) === 1,\n]);\n```\n\n### Resource Routing\n\n`RestRouter` can declare standard REST resource routes:\n\n```php\n$router-\u003eapiResource('/books', BookRestController::class);\n```\n\nFor non-numeric identifiers:\n\n```php\n$router-\u003eapiResource('/books', BookRestController::class, args: [\n    'id_pattern' =\u003e '[a-zA-Z0-9_-]+',\n]);\n```\n\nGenerated routes:\n\n```txt\nGET    /books\nPOST   /books\nGET    /books/{id}\nPUT    /books/{id}\nPATCH  /books/{id}\nDELETE /books/{id}\n```\n\n### Policies\n\nControllers can use an injected policy through the protected `$policy` property and call `authorize()`.\n\n`ResourcePolicy` uses WordPress `read` for reads by default, `edit_posts` for create, `edit_post` for update and `delete_post` for delete. If a resource should be publicly readable, override `viewAnyCapability()` and `viewCapability()` to return `null`.\n\n## CLI\n\nThe package exposes a Composer binary named `wpp`:\n\n```bash\nvendor/bin/wpp make:post-type Product\nvendor/bin/wpp make:repository Product\nvendor/bin/wpp make:taxonomy ProductCategory\nvendor/bin/wpp make:shortcode ProductCatalog\nvendor/bin/wpp make:rest-controller Product\nvendor/bin/wpp make:routes Api\nvendor/bin/wpp make:resource Product\nvendor/bin/wpp make:crud Product\nvendor/bin/wpp make:admin-crud Product\nvendor/bin/wpp make:provider Billing\nvendor/bin/wpp make:admin-page Settings\n```\n\nUseful options:\n\n```bash\nvendor/bin/wpp make:post-type Book --plugin=/path/to/plugin --namespace=MyPlugin\n```\n\n`make:resource` generates a CPT, repository, read-only REST controller and routes file. `make:crud` generates the full REST CRUD stack. `make:admin-crud` generates `config/admin-crud.php` and admin `index/form` views.\n\nThe generator creates application files only. You still declare the post type, route file and/or `admin_crud` in `config/plugin.php`, keeping configuration explicit.\n\n## Tests\n\nThe project contains unit tests for the container, REST router, validation, views, CLI generator and the most sensitive providers using local WordPress stubs.\n\nLocal PHP/Composer:\n\n```bash\ncomposer install\nvendor/bin/phpunit\n```\n\nDocker:\n\n```bash\ndocker compose run --rm tests\n```\n\nQuality checks:\n\n```bash\ncomposer cs:check\ncomposer cs:fix\ncomposer analyse\ncomposer test\ncomposer check:composer\ncomposer ci\n```\n\nCI definitions are provided for GitHub Actions and GitLab CI:\n\n```txt\n.github/workflows/tests.yml\n.gitlab-ci.yml\n```\n\nPipelines run Composer validation, PHP CS Fixer, PHPStan and PHPUnit.\n\n## Releases\n\nThe project follows SemVer. Public versions should be created with Git tags (`v0.9.0`, `v1.0.0`, etc.) rather than a `version` field in `composer.json`.\n\nChanges are tracked in `CHANGELOG.md`, and the release process is documented in `RELEASE.md`.\n\nStability target:\n\n- `0.9.x`: pilot production use in internal plugins;\n- `1.0.0`: public API freeze for the main framework surface.\n\n## Uninstall\n\nDeletion cleanup is declarative:\n\n```php\n'uninstall' =\u003e [\n    'options' =\u003e [\n        'my_plugin_version',\n        'my_plugin_settings',\n    ],\n    'site_options' =\u003e [],\n    'remove_capabilities' =\u003e true,\n],\n```\n\n## Example\n\nA concrete example is available in:\n\n```txt\nexamples/product-catalog-plugin/\n```\n\nIt creates a `product` custom post type, `product_category` taxonomy, settings page, admin CRUD, metaboxes, REST routes and displays products with:\n\n```txt\n[product_catalog]\n```\n\nExample REST routes:\n\n```txt\n/wp-json/products/v1/products\n/wp-json/products/v1/products/{id}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaml-ns%2Fwpp-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyaml-ns%2Fwpp-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaml-ns%2Fwpp-framework/lists"}