# Mantle by Alley > Mantle is a Laravel-inspired framework for building large, robust websites and applications with WordPress. These docs cover installation, architecture, routing and HTTP, Eloquent-style models, queues, templating, and testing. ## Docs ### Architecture #### Introduction[​](#introduction "Direct link to Introduction") Mantle is a modern PHP framework that is designed to be simple, flexible, and extensible. It includes a powerful service container with dependency injection. #### Service Container[​](#service-container "Direct link to Service Container") The service container is a powerful tool for managing class dependencies and performing dependency injection with ease. Most function calls are performed through the service container and support dynamic dependency injection without any need to configure it. Here's an example of the container with no class resolution needed: ```php use Mantle\Http\Controller; class Example_Service { } // Then you can instantiate the service from the container as a dependency. class Example_Controller extends Controller { public function __construct( Example_Service $service ) { $this->service = $service; } } ``` In the above example, the `Example_Service` class is automatically resolved because it is type-hinted in the constructor of the `Example_Controller` class. It has no dependencies, so it is automatically resolved and injected into the controller. Most classes in the framework are resolved through the service container and can be type-hinted in the same manner. For example, we can type-hint the `Request` class in a controller's constructor and the service container will automatically resolve it. ```php namespace App\Http\Controllers; use Mantle\Http\Controller; class Example_Controller extends Controller { public function index( Request $request ) { // ... } } ``` Most users will not need to interface with the container directly or register anything with it. But it is available to you if you need it. For more information about the service container and the underlying concept of Dependency Injection, read this document on [Understanding Dependency Injection](https://php-di.org/doc/understanding-di.html). ##### Binding to the Container[​](#binding-to-the-container "Direct link to Binding to the Container") ###### Binding[​](#binding "Direct link to Binding") You can bind a class to the container by using the `bind` method. This will allow you to resolve an instance of the class when it is being instantiated through the container. ```php $this->app->bind( Example_Service::class, function( $app ) { return new Example_Service(); }); ``` Every time that `Example_Service` is resolved from the container, a new instance of `Example_Service` will be created and resolved through the closure. You may also use the `bind_if` method to only bind the class if it has not already been bound. ```php $this->app->bind_if( Example_Service::class, function( $app ) { return new Example_Service(); }); ``` ###### Singleton[​](#singleton "Direct link to Singleton") You can also bind a singleton instance of a class to the container that will return the same instance every time it is resolved. ```php $this->app->singleton( Example_Service::class, function( $app ) { return new Example_Service(); }); ``` You may also use the `singleton_if` method to only bind the singleton if it has not already been bound. ```php $this->app->singleton_if( Example_Service::class, function( $app ) { return new Example_Service(); }); ``` ###### Interfaces[​](#interfaces "Direct link to Interfaces") You can bind an interface to a concrete class in the container. This will allow you to resolve the interface and get an instance of the concrete class. A common pattern for this is to declare a feature with an interface and then bind the implementation of the interface in the container. ```php $this->app->bind( Example_Interface::class, Example_Service::class ); ``` You can then type-hint the interface in your classes and the container will resolve the concrete class. ```php class Example_Controller extends Controller { public function __construct( Example_Interface $service ) { $this->service = $service; } } ``` ##### Resolving from the Container[​](#resolving-from-the-container "Direct link to Resolving from the Container") ###### Using `make`[​](#using-make "Direct link to using-make") You can resolve a class from the container by using the `make` method. This will return an instance of the class with all of its dependencies resolved. ```php $service = $this->app->make( Example_Service::class ); ``` If the class has dependencies, they will be resolved and injected into the instance. If the class has dependencies that are not resolvable via the container, you can pass them as an array of parameters to the `make_with` method. ```php $service = $this->app->make_with( Example_Service::class, [ 'param' => 'value' ] ); ``` You can also use the `app` helper function to resolve a class from the container. ```php app( Example_Service::class ); ``` You can also use the `App` facade to resolve a class from the container. ```php use Mantle\Facade\App; App::make( Example_Service::class ); ``` ###### Automatic Resolution[​](#automatic-resolution "Direct link to Automatic Resolution") When a class is type-hinted in a constructor, the container will automatically resolve the class and its dependencies. ```php class Example_Controller extends Controller { public function __construct( Example_Service $service ) { $this->service = $service; } } ``` #### Facades[​](#facades "Direct link to Facades") Facades are a static interface to the instances available from the service container. Instead of determining the underlying class or resolving it through the application, a facade will provide a single-line interface to call a singleton object from the container. ```php use Mantle\Facade\Config; echo Config::get( 'app.attribute' ); // Mantle\Contracts\Config\Repository ``` In this example, the config facade is a wrapper for the `config` singleton instance of `Mantle\Contracts\Config\Repository` instantiated from the service container. #### Aliases[​](#aliases "Direct link to Aliases") Aliases provide a root namespace level way of interfacing with classes in the framework. When combined with facades, they can provide a simple way of interfacing with singleton objects deep in the framework. ```php Log::info( 'My log message!' ); // Can be rewritten as... app()['log']->info( 'My log message!' ); ``` --- ### Bootloader The Mantle Bootloader class is responsible for instantiating the application/container and load the framework given the current context. It removes the need for boilerplate code to be included in projects but still allows for the flexibility to do so if they so choose. This works well to support running Mantle as apart of a `alleyinteractive/mantle`-based plugin OR in isolation in a larger codebase. The core of the bootloader is this: ```php bootloader()->boot(); ``` Running that line of code will instantiate the application and loading the framework given the current content. You can also use the bootloader with your own custom application instance (like [we're doing in `alleyinteractive/mantle`](https://github.com/alleyinteractive/mantle/blob/main/bootstrap/app.php)): ```php use Mantle\Application\Application; $application = new Application(); // Perform some bindings, override some contracts, etc. // Boot the application with your custom instance and custom bindings. bootloader( $application )->boot(); ``` Mantle is flexible enough to require no application bindings or allow you to override all of them. It's up to you. The bootloader will attempt to boot the relevant application kernel given the current context. The kernel will then boot and setup the application. #### Supported Contexts[​](#supported-contexts "Direct link to Supported Contexts") ##### Web[​](#web "Direct link to Web") The web context will boot the HTTP application kernel (`Mantle\Framework\Http\Kernel`) which can be overridden by an application binding. The HTTP kernel will send the current request through [Mantle Routing](/docs/basics/requests.md) on the `parse_request` WordPress action. ##### Console[​](#console "Direct link to Console") Using the `bin/mantle` console application included with Mantle OR running a command via WP-CLI will boot the console kernel (`Mantle\Framework\Console`) which can also be overridden by an application binding. If we're running in WP-CLI mode, the application will register a `wp mantle` command that will proxy the request to the console application. If we're running the `bin/mantle` console application, the application will handle the request fully and then terminate. #### Providing Configuration[​](#providing-configuration "Direct link to Providing Configuration") Configuration can be provided in a number of ways. Mantle has a set of defaults that be overridden by configuration passed directly to the bootloader or by placing configuration files in the `config` directory [see example](https://github.com/alleyinteractive/mantle/tree/main/config). ```php bootloader() ->with_config( [ 'app' => [ 'namespace' => 'Custom\\Namespace', ], ] ) ->boot(); ``` In the above example, we're overriding the default application namespace with `Custom\\Namespace`. All other configuration within 'app' will be merged with the [framework's default configuration](https://github.com/alleyinteractive/mantle-framework/blob/1.x/config/app.php). #### Registering Custom Service Providers[​](#registering-custom-service-providers "Direct link to Registering Custom Service Providers") You can pass a custom service provider to the bootloader's configuration that will be merged and loaded with the default service providers. ```php bootloader() ->with_providers( [ Custom\ServiceProvider::class, ] ) ->boot(); ``` #### Providing Kernels[​](#providing-kernels "Direct link to Providing Kernels") You can also provide your own application kernel to the bootloader. This is useful if you want to override the default kernel or provide your own functionality. This is optional and Mantle will use the default kernel if none is provided. ```php bootloader() ->with_kernel( console: Custom\Console\Kernel::class, http: Custom\Http\Kernel::class, ) ->boot(); ``` The console kernel must implement `Mantle\Contracts\Console\Kernel` and the HTTP kernel must implement `Mantle\Contracts\Http\Kernel`. #### Registering Routes[​](#registering-routes "Direct link to Registering Routes") Routes can be loaded and registered via the bootloader. Mantle supports a number of different ways to register routes with the most common being passing a file path to the bootloader to load routes from. ```php bootloader() ->with_routes( web: __DIR__ . '/../routes/web.php', rest_api: __DIR__ . '/../routes/rest-api.php', ) ->boot(); ``` Mantle also supports passing a closure to the bootloader to register routes dynamically: ```php use Mantle\Contracts\Http\Routing\Router; bootloader() ->with_routes( function ( Router $router ): void { $router->get( '/example', function () { return 'Hello, World!'; } ); } ) ->boot(); ``` #### Registering Custom Bindings[​](#registering-custom-bindings "Direct link to Registering Custom Bindings") Custom container bindings can be registered via the bootloader. This is useful for overriding default Mantle bindings or providing your own functionality. ```php bootloader() ->bind( 'example', function () { return new Example(); } ) ->boot(); ``` --- ### Configuration ### Introduction Mantle provides a configuration interface to allow easy control over the application and how it differs on each environment. The application ships with a few configuration files in the `config/` folder. #### Getting Configuration[​](#getting-configuration "Direct link to Getting Configuration") Configuration is possible through the `config()` helper function, the `Config` alias, and `Mantle\Facade\Config` facade. Each method supports a "dot" syntax which includes the name of the file and the option you wish to access. ```php config( 'app.providers' ); Config::get( 'app.providers' ); Mantle\Facade\Config::get( 'app.providers' ); ``` #### Environment Configuration[​](#environment-configuration "Direct link to Environment Configuration") It is often helpful to have different configuration values based on the environment where the application is running. For example, you may wish to use a different cache driver locally than you do on your production server. To make this a simple, Mantle utilizes the [DotEnv](https://github.com/vlucas/phpdotenv) PHP library. In a fresh Mantle installation, the root directory of your application will contain a `.env.example` file that defines many common environment variables. This file should be copied to `.env` for use. Deploying your site to a server? See [Environment File Location](#environment-file-location) for where the `.env` should be placed. Mantle's default `.env` file contains some common configuration values that may differ based on whether your application is running locally or on a production web server. These values are then retrieved from various Mantle configuration files within the config directory using Mantle's `environment()` function. If you are developing with a team, you may wish to continue including a `.env.example` file with your application. By putting placeholder values in the example configuration file, other developers on your team can clearly see which environment variables are needed to run your application. tip Any variable in your .env file can be overridden by external environment variables such as server-level or system-level environment variables. ##### Environment File Location[​](#environment-file-location "Direct link to Environment File Location") For most WordPress installations, all files static files will be exposed to the web server and accessible by the end user. Placing a `.env` file in your plugin's folder is not secure since that file can be accessed by anybody on the internet with a unique interest in your site. Mantle supports placing the `.env` file in more secure locations in your WordPress application. For WordPress VIP users, the `.env` file can be placed inside your [private directory](https://docs.wpvip.com/technical-references/vip-codebase/private-directory/) (`/wp-content/private`). Mantle will also check in the `wp-content/private` and `WPCOM_VIP_PRIVATE_DIR` folder for `.env` files. For WordPress VIP Users, Mantle will also read the [Environmental Variables set by the platform](https://docs.wpvip.com/how-tos/manage-environment-variables/). ##### Environment File Security[​](#environment-file-security "Direct link to Environment File Security") Your `.env` file should not be committed to your application's source control, since each developer / server using your application could require a different environment configuration. Furthermore, this would be a security risk in the event an intruder gains access to your source control repository, since any sensitive credentials would get exposed. See [Environment File Location](#environment-file-location) for more information. ##### Environment Variable Types[​](#environment-variable-types "Direct link to Environment Variable Types") All variables in your .env files are typically parsed as strings, so some reserved values have been created to allow you to return a wider range of types from the env() function: | `.env` Value | `environment()` Value | | ------------ | --------------------- | | true | (bool) true | | (true) | (bool) true | | false | (bool) false | | (false) | (bool) false | | empty | (string) '' | | (empty) | (string) '' | | null | (null) null | | (null) | (null) null | If you need to define an environment variable with a value that contains spaces, you may do so by enclosing the value in double quotes: ```text APP_NAME="My Application" ``` ##### Retrieving Environment Configuration[​](#retrieving-environment-configuration "Direct link to Retrieving Environment Configuration") All of the variables listed in this file will be loaded into the `$_ENV` PHP super-global when your application receives a request. However, you may use the env helper to retrieve values from these variables in your configuration files. In fact, if you review the Mantle configuration files, you will notice many of the options are already using this helper: ```php 'debug' => environment( 'APP_DEBUG', defined( 'WP_DEBUG' ) && WP_DEBUG ), ``` The second value passed to the env function is the "default value". This value will be returned if no environment variable exists for the given key. #### File-based Environment-specific Configuration[​](#file-based-environment-specific-configuration "Direct link to File-based Environment-specific Configuration") For situations when environment variables aren't supported, environment-specific configuration is possible by including a configuration file in a child folder named after the respective environment to apply the configuration for. danger `.env`-based configuration is preferred for simplicity versus file-based PHP files. The following is an example of a configuration value that is only loaded on the `local` environment. ```text ├── README.md ├── config │   ├── app.php │   ├── local │   │   └── app.php ``` ```php // Located in config/local/app.php. return [ 'providers' => App\Providers\Local_Service_Provider::class, ]; ``` --- ### Service Provider Service providers act as the key method of extending the framework and bootstrapping essential services. Core application functionality will be defined and encapsulated entirely in a service provider. Your own application, as well as all core Mantle services, are bootstrapped via Service Providers. But, what do we mean by "bootstrapped"? In general, we mean registering things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure your application. ##### What would be a service provider?[​](#what-would-be-a-service-provider "Direct link to What would be a service provider?") A good example of what should be a service provider would be a feature in your application. For example, a network of sites have a feature to syndicated content between sites. The feature could be wrapped into a single service provider which would bootstrap and setup all required services. The service provider would be the starting point for that feature. ##### Registering a Service Provider[​](#registering-a-service-provider "Direct link to Registering a Service Provider") The application stores service providers in the `config/app.php` file includes with Mantle. There is a `providers` array of classes in that file that include all providers the application will initialize on each request. ```php // Inside of config/app.php... 'providers' => [ // Add provider here... App\Providers\App_Service_Provider::class, ], ``` Automatic Registration Mantle will automatically register service providers via [Automatic Registration with WordPress Events](#automatic-registration-with-wordpress-events). #### Writing a Service Provider[​](#writing-a-service-provider "Direct link to Writing a Service Provider") Service providers extend the `Mantle\Support\Service_Provider` class and include a `register` and `boot` method. The `register` method is used to register application services with the application container. The `boot` method is used to boot provider, setup any classes, register any WordPress hooks, etc. The `boot` method should always call the parent boot method via `parent::boot();`. A service provider can be generated for you by running the following command: ```bash ./bin/mantle make:provider ``` ##### Service Provider Structure[​](#service-provider-structure "Direct link to Service Provider Structure") ```php namespace App; use Mantle\Support\Service_Provider; /** * Example_Provider Service Provider */ class Example_Provider extends Service_Provider { /** * Register the service provider. */ public function register() { // Register the provider. $this->app->singleton( 'binding-to-register', function( $app ) { return new Essential_Service( $app ); } ); } /** * Boot the service provider. */ public function boot() { parent::boot(); // Boot the provider. } } ``` #### Automatic Registration with WordPress Events[​](#automatic-registration-with-wordpress-events "Direct link to Automatic Registration with WordPress Events") Service providers are the heart of your application. They can register services and add event listeners that are fired when specific events/actions happen in WordPress. Mantle supports automatic registration of event listeners using PHP attributes: ```php use Mantle\Support\Attributes\Action; class Example_Provider extends Service_Provider { #[Action('wp_loaded')] public function handle_loaded_event() { // Called on wp_loaded at priority 10. } #[Action('admin_screen', 20)] public function handle_admin_screen_event() { // Called on admin_screen at priority 20. } } ``` The service provider also supports magic methods via [Hookable](/docs/features/support/hookable.md) that automatically register provider methods with WordPress actions and filters. The service provider will automatically register hooks for the provider by checking for methods that use the format of `on_{hook}` and `on_{action}_at_{priority}` as method names. Both actions and filters are supported using the same method naming convention. ```php use Mantle\Support\Attributes\Action; class Example_Provider extends Service_Provider { #[Action('wp_loaded')] public function register_services(): void { // Register any services here on wp_loaded. } public function on_wp_loaded() { // Called on wp_loaded at priority 10. } public function on_admin_screen_at_99() { // Called on admin_screen at priority 99. } } ``` See [Hookable](/docs/features/support/hookable.md) for more information. #### Conditional Loading of a Service Provider[​](#conditional-loading-of-a-service-provider "Direct link to Conditional Loading of a Service Provider") A service provider can be conditionally loaded using a [validator attribute](/docs/features/types.md#validator-attribute) on the service provider class. If the service provider uses a validator attribute, the provider will only be loaded if the validator's `validate` method passes. ```php namespace App; use Mantle\Support\Service_Provider; use Mantle\Types\Attributes\Environment; /** * Example Provider that only loads in production environment. */ #[Environment('production')] class Example_Provider extends Service_Provider { // Provider code... } ``` For more information on creating custom validator attributes, see [Validator Attribute](/docs/features/types.md#validator-attribute). --- ### Blade Templating Mantle brings the power and elegance of [Laravel's Blade templating engine](https://laravel.com/docs/master/blade) to WordPress. Blade provides a clean, powerful templating syntax that makes it easy to build dynamic views while maintaining the flexibility you need for WordPress development. Blade templates can be used seamlessly alongside traditional PHP templates in Mantle. The framework automatically detects the file extension and uses the appropriate template engine, giving you the freedom to choose the right tool for each situation. #### Getting Started[​](#getting-started "Direct link to Getting Started") ##### Basic Blade Syntax[​](#basic-blade-syntax "Direct link to Basic Blade Syntax") Blade templates use the `.blade.php` file extension and support all standard Blade syntax: views/welcome.blade.php ```php

Welcome to {{ $site_name }}

Today is {{ date('Y-m-d') }}

@if ($user_logged_in)

Hello, {{ $current_user->display_name }}!

@else

Please log in

@endif ``` ##### Displaying Data[​](#displaying-data "Direct link to Displaying Data") Use double curly braces to display data. Blade automatically escapes the output to prevent XSS attacks: ```php {{-- Escaped output (safe) --}}

{{ $post->post_title }}

{{ $post->post_content }}

{{-- Raw, unescaped output (use with caution) --}}
{!! $post->post_content !!}
``` ##### Accessing WordPress Functions[​](#accessing-wordpress-functions "Direct link to Accessing WordPress Functions") You can call WordPress functions directly in Blade templates: views/post-meta.blade.php ```php

{{ the_title() }}

{!! the_content() !!}
@if(has_post_thumbnail()) @endif
``` #### Control Structures[​](#control-structures "Direct link to Control Structures") Blade provides elegant alternatives to PHP's control structures: ##### Conditional Statements[​](#conditional-statements "Direct link to Conditional Statements") ```php @if (is_user_logged_in())

Welcome back!

@elseif(is_front_page())

Welcome to our homepage!

@else

Hello, visitor!

@endif {{-- Short conditional --}} @auth

You are logged in

@endauth @guest

Please log in

@endguest {{-- Custom conditional --}} @unless(is_admin())

This is not the admin area

@endunless ``` ##### Loops[​](#loops "Direct link to Loops") ```php @foreach ($posts as $post)

{{ $post->post_title }}

{{ wp_trim_words($post->post_content, 20) }}

@endforeach @forelse ($comments as $comment)

{{ $comment->comment_author }}

{{ $comment->comment_content }}

@empty

No comments yet.

@endforelse @for ($i = 1; $i <= 5; $i++)

Item {{ $i }}

@endfor @while (have_posts()) {{ the_post() }}

{{ the_title() }}

{!! the_content() !!}
@endwhile ``` ##### Loop Variables[​](#loop-variables "Direct link to Loop Variables") Blade provides helpful loop variables: ```php @foreach($posts as $post)

Post #{{ $loop->iteration }}: {{ $post->post_title }}

@if($loop->even)

This is an even-numbered post

@endif

{{ $loop->remaining }} posts remaining

@endforeach ``` Available loop variables: * `$loop->index` - The index of the current loop iteration (starts at 0) * `$loop->iteration` - The current loop iteration (starts at 1) * `$loop->remaining` - The iterations remaining in the loop * `$loop->count` - The total number of items in the array * `$loop->first` - Whether this is the first iteration * `$loop->last` - Whether this is the last iteration * `$loop->even` - Whether this is an even iteration * `$loop->odd` - Whether this is an odd iteration * `$loop->depth` - The nesting level of the current loop * `$loop->parent` - When in a nested loop, the parent's loop variable #### Template Inheritance[​](#template-inheritance "Direct link to Template Inheritance") One of Blade's most powerful features is template inheritance, allowing you to create a master layout and extend it: ##### Creating a Layout[​](#creating-a-layout "Direct link to Creating a Layout") views/layouts/app.blade.php ```php @yield('title', get_bloginfo('name')) {{ wp_head() }}
@yield('content')
{{ wp_footer() }} ``` ##### Extending the Layout[​](#extending-the-layout "Direct link to Extending the Layout") views/single.blade.php ```php @extends('layouts.app') @section('title', $post->post_title . ' - ' . get_bloginfo('name')) @section('content')

{{ $post->post_title }}

{!! apply_filters('the_content', $post->post_content) !!}
@if($post->tags) @endif
@include('partials.comments', ['post_id' => $post->ID]) @endsection ``` #### Including Sub-Views[​](#including-sub-views "Direct link to Including Sub-Views") Break your templates into smaller, reusable pieces: views/partials/post-card.blade.php ```php
@if(has_post_thumbnail($post->ID))
{!! get_the_post_thumbnail($post->ID, 'medium') !!}
@endif

{{ $post->post_title }}

{{ wp_trim_words($post->post_content, 30) }}
``` Include it in other templates: views/archive.blade.php ```php @extends('layouts.app') @section('content')
@foreach($posts as $post) @include('partials.post-card', ['post' => $post]) @endforeach
{{-- Pagination --}} @if($pagination) @endif @endsection ``` #### Working with WordPress Data[​](#working-with-wordpress-data "Direct link to Working with WordPress Data") ##### Setting the Global Post Object[​](#setting-the-global-post-object "Direct link to Setting the Global Post Object") When working with WordPress template functions, you may need to set the global post object: routes/web.php ```php Route::get('/article/{post}', function (Post $post) { return view('single') ->with('post', $post) ->set_post($post); // Sets the global $post object }); ``` views/single.blade.php ```php
{{-- These WordPress functions now work because $post is set globally --}}

{{ the_title() }}

{{ the_content() }}
{{-- You can also use the passed variable --}}

Published: {{ $post->post_date }}

``` ##### Using the Loop Helper[​](#using-the-loop-helper "Direct link to Using the Loop Helper") Mantle provides helpful functions for working with post collections: ExampleController.php ```php public function index() { $posts = Post::published()->take(10)->get(); return view('post-list', [ 'posts' => $posts ]); } ``` views/post-list.blade.php ```php
{!! loop($posts, 'partials.post-card') !!}
``` ##### Custom Loops with Template Parts[​](#custom-loops-with-template-parts "Direct link to Custom Loops with Template Parts") views/homepage.blade.php ```php @extends('layouts.app') @section('content')

Recent Posts

{!! loop(Post::recent()->limit(6)->get(), 'partials.post-card') !!}
@endsection ``` #### Advanced Features[​](#advanced-features "Direct link to Advanced Features") ##### Custom Blade Directives[​](#custom-blade-directives "Direct link to Custom Blade Directives") You can create custom Blade directives for common WordPress patterns: app/Providers/View\_Service\_Provider.php ```php use Mantle\Facade\Blade; class View_Service_Provider extends Service_Provider { public function boot() { // Custom directive for WordPress capability checks Blade::directive('can', function ($capability) { return ""; }); Blade::directive('endcan', function () { return ""; }); // Custom directive for WordPress nonces Blade::directive('nonce', function ($action) { return ""; }); } } ``` Use your custom directives: ```php @can('edit_posts') Create New Post @endcan
@nonce('my_form_action')
``` ##### Render Blade Dynamically[​](#render-blade-dynamically "Direct link to Render Blade Dynamically") Mantle provides a convenient way to render Blade templates directly from strings using `Blade::render_string()`. This is useful when you need to generate templates dynamically at runtime, rather than from static files. ###### Example[​](#example "Direct link to Example") ```php use Mantle\Facade\Blade; // Render a Blade template from a string $output = Blade::render_string( 'Hello, {{ $name }}!', [ 'name' => 'World' ] ); // $output: "Hello, World!" ``` note When rendering templates from strings, ensure that any user-provided content is properly escaped to prevent security issues. #### File Organization[​](#file-organization "Direct link to File Organization") Organize your Blade templates in a logical structure: ```text views/ ├── layouts/ │ ├── app.blade.php │ ├── admin.blade.php │ └── email.blade.php ├── pages/ │ ├── home.blade.php │ ├── about.blade.php │ └── contact.blade.php ├── posts/ │ ├── single.blade.php │ ├── archive.blade.php │ └── search.blade.php ├── partials/ │ ├── header.blade.php │ ├── footer.blade.php │ ├── navigation.blade.php │ ├── post-card.blade.php │ └── sidebar.blade.php ├── components/ │ ├── alert.blade.php │ ├── modal.blade.php │ └── form-field.blade.php └── emails/ ├── welcome.blade.php └── notification.blade.php ``` #### Integration with WordPress Themes[​](#integration-with-wordpress-themes "Direct link to Integration with WordPress Themes") Blade templates work seamlessly in WordPress themes. You can gradually migrate from PHP templates to Blade: ##### Theme Template Hierarchy[​](#theme-template-hierarchy "Direct link to Theme Template Hierarchy") Blade templates follow WordPress template hierarchy: ```text themes/your-theme/ ├── index.blade.php (fallback template) ├── home.blade.php (homepage) ├── single.blade.php (single posts) ├── page.blade.php (static pages) ├── archive.blade.php (archives) ├── search.blade.php (search results) ├── 404.blade.php (not found) └── single-{post-type}.blade.php (custom post types) ``` ##### Mixed Template Usage[​](#mixed-template-usage "Direct link to Mixed Template Usage") You can use both PHP and Blade templates in the same theme: ```php // WordPress will use single.blade.php if it exists // Otherwise it falls back to single.php themes/your-theme/ ├── single.blade.php ← Blade template (preferred) ├── single.php ← PHP fallback ├── page.blade.php ← Blade template └── archive.php ← PHP template ``` #### Conclusion[​](#conclusion "Direct link to Conclusion") Blade templating brings modern, clean syntax to WordPress development while maintaining full compatibility with WordPress features and functions. By leveraging Blade's powerful features like template inheritance, components, and control structures, you can create more maintainable and elegant templates for your WordPress applications. For more information about templating in Mantle, see the [Templating and Views documentation](/docs/basics/templating.md). To learn more about Blade syntax and features, refer to the [official Laravel Blade documentation](https://laravel.com/docs/master/blade). --- ### Console Command #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides a console application that is integrated with WP-CLI to make running commands easier. Out of the box, Mantle includes a `bin/mantle` console application that can do a number of things without connecting to WordPress or the database. ```bash bin/mantle ``` The `bin/mantle` application allows you to generate classes, models, etc. as well as discover the current state of the application. For example, you can run `bin/mantle model:discover` to automatically register all the models in the application without needing to install WordPress. This is incredibly useful in a CI/CD environment where you want to prepare the application for deployment. The application also integrates with WP-CLI. Running `wp mantle ` will provide an even larger set of commands that can be run through the application and interact with WordPress/the database. ##### Generating a Command[​](#generating-a-command "Direct link to Generating a Command") To generate a new command, you may use the `make:command` command. This command will create a new command class in the `app/console` directory. The command will also be automatically discovered and registered by the application. ```bash bin/mantle make:command ``` ##### Command Structure[​](#command-structure "Direct link to Command Structure") After generating a command, you should verify the `$name` and `$synopsis` properties which determine the name and arguments/flags for the command, respectively. The `$synopsis` property is the `wp-cli` definition of the command's arguments ([reference the `wp-cli` documentation](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/)). The Mantle Service Container will automatically inject all dependencies that are type-hinted in the class. The following command would be registered to `wp mantle example_command`: ```php namespace App\Console; use Mantle\Console\Command; /** * Example_Command Controller */ class Example_Command extends Command { /** * The console command name. * * @var string */ protected $signature = 'example:my-command {argument} [--flag]'; /** * Callback for the command. */ public function handle() { // Write to the console. $this->line( 'Message to write.' ); // Error to the console. $this->error( 'Error message but does not exit without the second argument being true' ); // Ask for input. $question = $this->prompt( 'Ask a question?' ); $password = $this->secret( 'Ask a super secret question?' ); // Get an argument. $arg = $this->argument( 'name-of-the-argument', 'Default Value' ); // Get a flag. $flag = $this->flag( 'flag-to-get', 'default-value' ); } } ``` #### Registering a Command[​](#registering-a-command "Direct link to Registering a Command") Once generated, the commands should automatically be registered by the application. If for some reason that doesn't work or you wish to manually register commands, you can add them to the `app/console/class-kernel.php` file in your application: ```php namespace App\Console; use Mantle\Console\Kernel as Console_Kernel; /** * Application Console Kernel */ class Kernel extends Console_Kernel { /** * The commands provided by the application. * * @var array */ protected $commands = [ Command_To_Register::class, ]; } ``` #### Command Arguments / Options[​](#command-arguments--options "Direct link to Command Arguments / Options") Arguments and options can be included in the signature of the command. Arguments are required and options are optional. The signature is defined as a string with the following format: ```text {argument} [--option] ``` The arguments/options can be retrieved from the command's `argument( $key )`/`option( $key )` methods. ```php namespace App\Console; use Mantle\Console\Command; /** * Example Command */ class Example_Command extends Command { /** * The console command name. * * @var string */ protected $signature = 'example:my-command {argument} [--flag]'; /** * Callback for the command. */ public function handle() { $arg = $this->argument( 'argument' ); $flag = $this->flag( 'flag' ); // ... } } ``` #### Command Input[​](#command-input "Direct link to Command Input") A command can ask questions of the user to gather input. ##### `anticipate`[​](#anticipate "Direct link to anticipate") The `anticipate` method allows you to ask the user to type a value from a list of options. The method returns the value the user selects. ```php $value = $this->anticipate( 'What is your decision?', [ 'Moon', 'Sun' ] ); ``` ##### `ask`[​](#ask "Direct link to ask") The `ask` method asks the user for input. The method returns the value the user enters. ```php $value = $this->ask( 'What is your name?' ); ``` ##### `choice`[​](#choice "Direct link to choice") The `choice` method allows you to ask the user to select from a list of options. ```php $value = $this->choice( 'What is your decision?', [ 'Moon', 'Sun' ], $default_index ); ``` ##### `confirm`[​](#confirm "Direct link to confirm") The `confirm` method asks the user to confirm an action. The method returns a boolean value. ```php $confirmed = $this->confirm( 'Do you wish to continue?' ); ``` ##### `secret`[​](#secret "Direct link to secret") The `secret` method asks the user for a password or a secret value that won't appear in the console. ```php $password = $this->secret( 'What is the password?' ); ``` #### Command Output[​](#command-output "Direct link to Command Output") The `info`, `error`, `alert`, and `warn` methods can be used to write output to the console. ```php use Mantle\Console\Command; class Example_Command extends Command { public function handle() { $this->info( 'This is an informational message.' ); $this->error( 'This is an error message.' ); $this->alert( 'This is an alert message.' ); $this->warn( 'This is a warning message.' ); } } ``` A `fail()` method is also available to write an error message and exit the command. ```php use Mantle\Console\Command; class Example_Command extends Command { public function handle() { $this->fail( 'This is an error message.' ); } } ``` ##### Colors[​](#colors "Direct link to Colors") You can use the `colorize()` method to add color to your output. The method accepts a string and a color. ```php use Mantle\Console\Command; class Example_Command extends Command { public function handle() { $this->line( $this->colorize( 'This is a red message.', 'red' ) ); } } ``` ##### Progress Bars[​](#progress-bars "Direct link to Progress Bars") You can use the `with_progress_bar()` method to display a progress bar. The method accepts a closure that will be called for each iteration of the progress bar. The closure should return the number of items processed. ```php use Mantle\Console\Command; class Example_Command extends Command { public function handle() { $this->with_progress_bar( get_posts( [...], function( \WP_Post $post ) { // Process the post. return 1; } ) ); } } ``` #### Advanced Output[​](#advanced-output "Direct link to Advanced Output") Underneath the hood, `$this->output` is an instance of `Symfony\Component\Console\Style\SymfonyStyle` which provides a number of methods for writing to the console. You can use these methods directly if you need more control over the output. See [the Symfony documentation](https://symfony.com/doc/current/console/style.html) for more information. --- ### Helpers Mantle includes a variety of global "helper" functions (props to Laravel) to make life easier. The [`mantle-framework/support` package includes a number of namespaced-helpers](/docs/features/support/helpers.md) that are available for use in your application. #### Application Helpers[​](#application-helpers "Direct link to Application Helpers") ##### `app()`[​](#app "Direct link to app") Retrieve the global Mantle Application Container or a specific binding on the container. ```php app(); app( Specific_Binding::class ); ``` ##### `config()`[​](#config "Direct link to config") Retrieve a configuration value for the application in a dot-notation. ```php config( 'app.value-to.get', 'default value' ); ``` ##### `base_path()`[​](#base_path "Direct link to base_path") Retrieve the base path to the application. ```php base_path(); ``` ##### `response()`[​](#response "Direct link to response") Helper to build a response for a route (see 'Requests Lifecycle'). ```php response()->view( 'view/to/load' ); response()->json( [ 1, 2, 3 ] ); ``` ##### `view()`[​](#view "Direct link to view") Return a new instance of a view. ```php echo view( 'view-to-load', [ 'variable' => 123 ] ); ``` ##### `loop()`[​](#loop "Direct link to loop") Loop over a collection/array of post objects. Supports a collection or array of `WP_Post` objects, Mantle Models, post IDs, or a `WP_Query` object. ```php $posts = Post::all(); echo loop( $posts, 'view-to-load' ); ``` ##### `iterate()`[​](#iterate "Direct link to iterate") Iterate over a collection/array of arbitrary data. Each view is passed `index` and `item` as a the current item in the loop. ```php echo iterate( [ 1, 2, 3 ], 'view-to-load' ); ``` ##### `mantle_get_var()`[​](#mantle_get_var "Direct link to mantle_get_var") Get the variable for a template part. ```php mantle_get_var( 'index', 'default-value' ); ``` ##### `route()`[​](#route "Direct link to route") Get a URL to a specific route. ```php route( 'route-name' ); ``` ##### `abort()`[​](#abort "Direct link to abort") Throw a HTTP exception with a specific status code inside of a route. ```php abort( 404 ); abort( 400, 'Invalid arguments sent!' ); ``` ##### `abort_if()` and `abort_unless()`[​](#abort_if-and-abort_unless "Direct link to abort_if-and-abort_unless") Abort if the given condition passes or fails a truth test. ```php abort_if( $value_to_check, 404 ); abort_unless( $value_to_check, 404 ); ``` #### Array Helpers[​](#array-helpers "Direct link to Array Helpers") The `Mantle\Support\Arr` class contains all the Laravel array helper methods you might be familiar with (some methods have been renamed to match WordPress coding standards). You can reference those [here](https://laravel.com/docs/10.x/helpers#arrays). --- ### Requests and Routing Mantle provides a MVC framework on-top of WordPress. You can add a route fluently and send a response straight back without needing to work with WordPress's `add_rewrite_rule()` at all. routes/web.php ```php Route::get( '/example-route', function() { return 'Welcome!'; } ); Route::get( '/hello/{who}', function( $name ) { return "Welcome {$name}!"; } ); ``` Web routes are defined in the `routes/web.php` file and [REST API routes](/docs/basics/requests.md#rest-api-routing) are defined in the `routes/rest-api.php` file. Routes are loaded via the bootloader's `with_routing()` method: bootstrap/app.php ```php use Mantle\Framework\Bootloader; return Bootloader::create() ->with_kernels( console: App\Console\Kernel::class, http: App\Http\Kernel::class, ) ->with_exception_handler( App\Exceptions\Handler::class ) ->with_routing( web: __DIR__ . '/../routes/web.php', rest_api: __DIR__ . '/../routes/rest-api.php', pass_through: true, ); ``` #### Registering Routes[​](#registering-routes "Direct link to Registering Routes") Routes are registered for the application in the `routes/` folder of the application. Underneath all of it, routes are a wrapper on-top of [Symfony routing](https://symfony.com/doc/current/routing.html) with a fluent-interface on top. ##### Closure Routes[​](#closure-routes "Direct link to Closure Routes") At its most basic level, routes can be a simple anonymous function. routes/web.php ```php Route::get( '/endpoint', function() { return 'Hello!'; } ); ``` ##### Controller Routes[​](#controller-routes "Direct link to Controller Routes") You can use a controller to handle routes as well. In the future, resource and automatic controller routing will be added. routes/web.php ```php Route::get( '/controller-endpoint', Controller_Class::class . '@method_to_invoke' ); Route::get( '/controller-endpoint', [ Controller_Class::class, 'method_to_invoke' ] ); ``` ###### Generating a Controller[​](#generating-a-controller "Direct link to Generating a Controller") A controller can be generated through the CLI. ```bash bin/mantle make:controller ``` ###### Invokable Controllers[​](#invokable-controllers "Direct link to Invokable Controllers") A single-method controller is supported by defining a controller with an `__invoke` method. ```php use Mantle\Http\Controller; class Invokable_Controller extends Controller { /** * Method to run. */ public function __invoke() { return [ ... ]; } } ``` Invokable controllers can be registered by passing the controller class name to the router. ```php Route::get( '/example-route', Invokable_Controller::class ); ``` Invokable controllers are also generate-able through the CLI. ```bash wp mantle make:controller --invokable ``` #### Available Router Methods[​](#available-router-methods "Direct link to Available Router Methods") The router has all HTTP request methods available: ```php use Mantle\Facade\Route; Route::get( $uri, $callback ); Route::post( $uri, $callback ); Route::put( $uri, $callback ); Route::patch( $uri, $callback ); Route::delete( $uri, $callback ); Route::options( $uri, $callback ); ``` #### Route Parameters[​](#route-parameters "Direct link to Route Parameters") Routes can have parameters that are passed to the callback function from variables in the URI. The parameters are defined by wrapping the variable name in curly braces. routes/web.php ```php Route::get( '/post/{slug}', function( $slug ) { return "Post slug: {$slug}"; } ); ``` ##### Required Parameters[​](#required-parameters "Direct link to Required Parameters") Parameters are required by default. If a parameter is not provided, the application will return a 404 response. ##### Optional Parameters[​](#optional-parameters "Direct link to Optional Parameters") You can make a parameter optional by adding a `?` after the parameter name. routes/web.php ```php Route::get( '/post/{slug?}', function( $slug = null ) { return "Post slug: {$slug}"; } ); ``` #### Named Routes[​](#named-routes "Direct link to Named Routes") Naming a route provides an easy-to-reference way of generating URLs for a route. ```php Route::get( '/post/{slug}', function() { // } )->name( 'route-name' ); ``` Routes can also pass the name to the router through as an array. ```php Route::get( '/posts/{slug}', [ 'name' => 'named-route', 'callback' => function() { ... }, ] ); ``` #### Generating URLs to Named Routes[​](#generating-urls-to-named-routes "Direct link to Generating URLs to Named Routes") Once a route has a name assigned to it, you may use the route's name when generating URLs or redirects via the helper `route` function. ```php $url = route( 'route-name' ); ``` #### Route Middleware[​](#route-middleware "Direct link to Route Middleware") Middleware can be used to filter incoming requests and the response sent to the browser. Think of it like a WordPress filter on top of the request and the end response. ##### Example Middleware[​](#example-middleware "Direct link to Example Middleware") ```php /** * Example_Middleware class file. * * @package Mantle */ namespace App\Middleware; use Closure; use Mantle\Http\Request; /** * Example Middleware */ class Example_Middleware { /** * Handle the request. * * @param Request $request Request object. * @param Closure $next Callback to proceed. * @return \Mantle\Http\Response */ public function handle( Request $request, Closure $next ) { // Modify the request or bail early. $request->setMethod( 'POST' ); /** * @var Mantle\Http\Response */ $response = $next( $request ); // Modify the response. $response->headers->set( 'Special-Header', 'Value' ); return $response; } } ``` ##### Authentication Middleware[​](#authentication-middleware "Direct link to Authentication Middleware") Included with Mantle, a route can check a user's capability before allowing them to view a page. ```php use Mantle\Facade\Route; Route::get('/route-to-protect', function() { // The current user can 'manage_options'. } )->middleware( 'can:manage_options', Example_Middleware::class ); ``` ##### Removing Middleware[​](#removing-middleware "Direct link to Removing Middleware") Middleware can be removed from a route by using the `without_middleware` method. You can pass a single middleware, an array of middleware to remove, or remove all middleware. ```php use Mantle\Facade\Route; Route::get( '/route', function() { // ... } )->without_middleware( 'middleware_name' ); ``` Once common use case is to remove the wrap template middleware (which will wrap your response in a WordPress header/footer). You can use the `without_wrap_template()` method. ```php use Mantle\Facade\Route; Route::get( '/route', function() { // ... } )->without_wrap_template(); ``` #### Route Prefix[​](#route-prefix "Direct link to Route Prefix") Routes can be prefixed to make it easier to group routes together. ```php Route::prefix( 'prefix/to/use' )->group( function() { // Register a route with a prefix here! } ); ``` #### Requests Pass-Through to WordPress Routing[​](#requests-pass-through-to-wordpress-routing "Direct link to Requests Pass-Through to WordPress Routing") By default, requests will pass down to WordPress if there is no match in Mantle. That can be changed inside of the bootloader's `bootstrap/app.php` file. If the request doesn't have a match, the request will 404 and terminate before going through WordPress' require rules. REST API requests will always pass through to WordPress and bypass Mantle routing. #### Model Routing[​](#model-routing "Direct link to Model Routing") Models can have their permalinks and routing handled automatically. The underlying WordPress object will use the Mantle-generated URL for the model, too. The application will generate an archive route for post models and singular routes for post and term models. tip Models that are automatically registered will have their routing automatically registered. Read more about [model registration here](/docs/models/model-registration.md#model-routing). ##### Model Routing Controller[​](#model-routing-controller "Direct link to Model Routing Controller") Similar to a resource controller, the router will invoke a single controller for the 'resource' (the model). | Method | Callback | | --------------------------- | ----------------------------- | | Archive `/product/{slug}/` | `Product_Controller::index()` | | Singular `/product/{slug}/` | `Product_Controller::show()` | ##### Registering a Model for Routing[​](#registering-a-model-for-routing "Direct link to Registering a Model for Routing") Route models can be registered like any other route and also supports prefixes, middleware, etc. ```php Route::model( Product::class, Product_Controller::class ); ``` In the above example the `Product` model will have `/product/{slug}` and `/product` routes registered. ```php use Mantle\Database\Model\Post; use Mantle\Database\Model\Concerns\Custom_Post_Permalink; class Product extends Post { use Custom_Post_Permalink; public function get_route(): ?string { return '/route/{slug}'; } } ``` #### Route Model Binding[​](#route-model-binding "Direct link to Route Model Binding") Routes support model binding that will automatically resolve a model based on a route parameter and a type-hint on the route method. This supports implicit and explicit binding from a service provider. ##### Implicit Binding[​](#implicit-binding "Direct link to Implicit Binding") Mantle will automatically resolve models that are type-hinted for the route's method. For example: ```php Route::get( 'users/{user}', function ( App\User $user ) { return $user->title; } ); ``` Since the `$user` variable is type-hinted as `App\User` model and the variable name matches the `{user}` segment, Mantle will automatically inject the model instance that has an ID matching the corresponding value from the request URI. If a matching model instance is not found in the database, a 404 HTTP response will automatically be generated. ###### Customizing The Default Key Name[​](#customizing-the-default-key-name "Direct link to Customizing The Default Key Name") If you would like model binding to use a default database column other than id when retrieving a given model class, you may override the `get_route_key_name` method on the model: ```php /** * Get the route key for the model. * * @return string */ public function get_route_key_name(): string { return 'slug'; } ``` ##### Explicit Binding[​](#explicit-binding "Direct link to Explicit Binding") To register an explicit binding, use the router's model method to specify the class for a given parameter. You should define your explicit model bindings in the boot method of the `App_Service_Provider` class: ```php use Mantle\Facade\Route; public function boot() { parent::boot(); Route::bind_model( 'user', App\User::class ); } ``` Next, define a route that contains a `{user}` parameter: ```php Route::get( 'profile/{user}', function ( App\User $user ) { // } ); ``` Since we have bound all `{user}` parameters to the `App\User` model, a `User` instance will be injected into the route. So, for example, a request to `profile/1` will inject the `User` instance from the database which has an ID of `1`. If a matching model instance is not found in the database, a 404 HTTP response will be automatically generated. ###### Customizing The Resolution Logic[​](#customizing-the-resolution-logic "Direct link to Customizing The Resolution Logic") If you wish to use your own resolution logic, you may use the Route::bind method. The Closure you pass to the bind method will receive the value of the URI segment and should return the instance of the class that should be injected into the route: ```php /** * Bootstrap any application services. * * @return void */ public function boot() { parent::boot(); Route::bind( 'user', function ( $value ) { return App\User::where( 'name', $value )->firstOrFail(); } ); } ``` Alternatively, you may override the `resolve_route_binding` method on your model. This method will receive the value of the URI segment and should return the instance of the class that should be injected into the route: ```php /** * Retrieve the model for a bound value. * * @param mixed $value * @param string|null $field * @return static|null */ public function resolve_route_binding( $value, $field = null ) { return $this->where( 'name', $value )->firstOrFail(); } ``` #### Responses[​](#responses "Direct link to Responses") Responses for routed requests can come in all shapes and sizes. Underneath all of it, the response will always come out to be a `Symfony\Component\HttpFoundation\Response` object. The `response()` helper exists to help with returning responses. ###### Strings & Arrays[​](#strings--arrays "Direct link to Strings & Arrays") ```php use Mantle\Facade\Route; Route::get( '/', function () { return 'Hello World'; } ); Route::get( '/', function () { return [ 1, 2, 3 ]; } ); ``` ##### Views[​](#views "Direct link to Views") WordPress template parts can be returned for a route. ```php Route::get( '/', function () { return response()->view( 'template-parts/block', [ 'variable' => '123' ] ); } ); ``` ###### View Location[​](#view-location "Direct link to View Location") By default WordPress will only load a template part from the active theme and parent theme if applicable. Mantle supports loading views from a dynamic set of locations. Mantle support automatically register the current theme and parent theme as view locations. ###### Default View Locations[​](#default-view-locations "Direct link to Default View Locations") * Active Theme * Parent of Active Theme * `{root of mantle site}/views` For more information about views, read the 'Templating' documentation. ##### Redirects[​](#redirects "Direct link to Redirects") Redirects can be generated using the `response()` helper. ```php use Mantle\Facade\Route; Route::get( '/logout', function() { return response()->redirect_to( '/home' ); } ); Route::get( '/old-page', function() { return response()->redirect_to( '/home', 301 ); } ); // Redirects can also be done to a named route. Route::get( '/oh-no', function() { return response()->redirect_to_route( 'route_name' ); } ); ``` #### REST API Routing[​](#rest-api-routing "Direct link to REST API Routing") Mantle supports registering to the WordPress REST API directly. REST API Routes are registered underneath with native core functions and does not use the Symfony-based routing that web requests pass through. ##### Registering Routes[​](#registering-routes-1 "Direct link to Registering Routes") Registering a REST API route requires a different function call if you do not wish to use a closure. ```php use Mantle\Facade\Route; Route::rest_api( 'namespace/v1', '/route-to-use', function() { return [ 1, 2, 3 ]; } ); ``` Routes can also be registered using the same HTTP verbs web routes use with some minor differences. ```php use Mantle\Facade\Route; use WP_REST_Request; Route::rest_api( 'namespace/v1', function() { Route::get( '/example-group-get', function() { return [ 1, 2, 3 ]; } ); Route::get( '/example-with-param/(?P[a-z\-]+)', function( WP_REST_Request $request) { return $request['slug']; } ); } ); ``` REST API routes can also be registered using the same arguments you would pass to `register_rest_route()`. ```php use Mantle\Facade\Route; Route::rest_api( 'namespace/v1', function() { Route::get( '/example-endpoint', function() { // This callback can be omitted, too. }, [ 'permission_callback' => function() { // ... } ] ); } ); ``` ##### Using Controllers[​](#using-controllers "Direct link to Using Controllers") REST API routes can also use controllers to handle the request. You can use a specific method on a controller or make the controller invokeable to handle the request. ```php use Mantle\Facade\Route; use WP_REST_Request; Route::rest_api( 'namespace/v1', function() { Route::get( '/example-endpoint', Example_Controller::class ); Route::get( '/another-endpoint', [ Another_Example_Controller::class, 'method' ] ); } ); class Example_Controller { public function __invoke( WP_REST_Request $request ) { return [ 1, 2, 3 ]; } } class Another_Example_Controller { public function method( WP_REST_Request $request ) { return [ 1, 2, 3 ]; } } ``` ##### Using Middleware[​](#using-middleware "Direct link to Using Middleware") REST API routes also support the same [Route Middleware](#route-middleware) that web requests use. The only difference is that web requests are passed a Mantle Request object while REST API requests are passed a `WP_REST_Request` one. ```php use Mantle\Facade\Route; Route::middleware( Example_Middleware::class )->group( function() { Route::rest_api( 'namespace/v1', '/example-route', function() { /* ... */ } ); } ) ``` #### Events[​](#events "Direct link to Events") Mantle-powered routes for both web and REST API will fire the `Route_Matched` event when a route has been matched. It will always include the current request object. For web routes it will include the `Route` object as the route property. For REST API requests it will include an array of information about the current matched route. ```php use Mantle\Http\Routing\Events\Route_Matched; Event::listen( Route_Matched::class, function( Route_Matched $event ) { var_dump( $event->route ); var_dump( $event->request->ip() ); } ); ``` ##### New Relic[​](#new-relic "Direct link to New Relic") Using the `Route_Matched` event Mantle will automatically fill in transaction information for New Relic if the extension is loaded. All requests will have the transaction name properly formatted instead of relying on New Relic to fill in the blanks. --- ### Templating and Views Templating in WordPress should be delightful — Mantle hopes to make it that way. #### Views[​](#views "Direct link to Views") WordPress template parts can be returned for a route. routes/web.php ```php Route::get( '/', function () { return response()->view( 'template-parts/block', [ 'variable' => '123' ] ); } ); ``` ##### PHP Templates[​](#php-templates "Direct link to PHP Templates") Mantle supports normal PHP template partials. The `view()` helper function will automatically from any of the [configured view locations](#view-file-locations). ExampleController.php ```php class ExampleController { public function example() { return view( 'template-parts/block', [ 'variable' => '123' ] ); } } ``` template-parts/block.php ```php

``` ##### Blade Templates[​](#blade-templates "Direct link to Blade Templates") Mantle also supports loading [Laravel's Blade](https://laravel.com/docs/11.x/blade) template parts. Blade and WordPress template parts can be used interchangeably. Mantle uses the `illuminate/view` package directly to provide complete compatibility with Blade templating. ```php Hello, {{ $name }} ``` Blade templates can be used interchangeably with PHP templates. Mantle will automatically detect the file extension and load the appropriate template engine. For more information on Blade templating, see [Blade Templating](/docs/basics/blade.md). #### Passing Variables to Views[​](#passing-variables-to-views "Direct link to Passing Variables to Views") Frequently you will need to pass variables down to views from controllers and routes. To ensure a global variable isn't overwritten the variables are stored in the helper method `mantle_get_var()`. ```php // Call the view with a variable. echo view( 'template-parts/view', [ 'foo' => 'bar' ] ); ``` Inside the view: template-parts/view.php ```php
``` When using [Blade Templates](#blade-templates), variables can access the variables directly. Blade will automatically escape the contents of a variable when using `{{ ... }}`. ```php Hello {{ $foo }}! ``` You can also use `mantle_get_mixed_var()` to retrieve a variable as a [Mixed Data](/docs/features/support/mixed-data.md) instance: ```php Hello {{ mantle_get_mixed_var( 'foo' )->string() }}! ``` ##### Passing Global Variables[​](#passing-global-variables "Direct link to Passing Global Variables") Service Providers and other classes in the application can pass global variables to all views loaded. This can be very handy when you want to pass template variables to a service provider without doing any additional work in the route. ```php use Mantle\Facade\View; // Pass 'variable_to_pass' to all views. View::share( 'variable_to_pass', 'value or reference to pass' ); ``` #### Setting the Global Post Object[​](#setting-the-global-post-object "Direct link to Setting the Global Post Object") Commonly views need to set the global post object in WordPress for a view. This will allow WordPress template tags such as `the_ID()` and `the_title()` to work properly. routes/web.php ```php Route::get( '/article/{article}', function ( App\Article $article ) { // Supports passing a model, ID, or core WordPress object. return view( 'template-parts/block', [ 'post' => $article ] )->set_post( $article ); } ); ``` template-parts/block.blade.php ```php

{{ the_title() }}

{{ the_content() }}
{{-- Use the passed variable. --}}

{{ $post->post_title }}

``` #### View File Locations[​](#view-file-locations "Direct link to View File Locations") By default WordPress will only load a template part from the active theme and parent theme if applicable. Mantle supports loading views from a dynamic set of locations. Mantle support automatically register the current theme and parent theme as view locations. Additional paths can be registered through `View_Loader`. ```php use Mantle\Facade\View_Loader; View_Loader::add_path( '/path-to-add' ); // Optionally provide an alias for easy referencing. View_Loader::add_path( '/another-path', alias: 'vendor-views' ); // Remove a path. View_Loader::remove_path( '/path-to-remove' ); ``` Adding additional view locations will allow you to use templates that aren't located in the theme directory. This can be useful for loading templates from a plugin which WordPress doesn't natively support. ##### Default View Locations[​](#default-view-locations "Direct link to Default View Locations") * Active Theme * Parent of Active Theme (if applicable) * `{root of mantle site}/views` #### Loading Views[​](#loading-views "Direct link to Loading Views") Views can be loaded using the `view()` helper function. This function will automatically detect the file extension and load the appropriate template engine. ```php echo view( 'template-parts/block', [ 'variable' => '123' ] ); ``` Views will be loaded from the [configured view locations](#view-file-locations) in the order they were registered. If a view is found in multiple locations the first one found will be used. To load a view from a specific location you can prefix the view name with the alias of the location in the format `@{alias}/{path to view}`: ```php echo view( '@vendor-views/template-parts/block', [ 'variable' => '123' ] ); ``` This can be useful when you want to load views from a plugin or a specific directory that isn't the active theme or parent theme. #### View Methods[​](#view-methods "Direct link to View Methods") ##### `view()`[​](#view "Direct link to view") Load a view file and return the rendered output. This function will automatically detect the file extension and load the appropriate template engine (Blade or PHP). The view file will be loaded from the [configured view locations](#view-file-locations). ```php echo view( 'template-parts/block', [ 'variable' => '123' ] ); ``` ##### `loop()`[​](#loop "Direct link to loop") Loop over a collection/array of post objects. Supports a collection or array of `WP_Post` objects, Mantle Models, post IDs, or a `WP_Query` object. The post object will be automatically setup for each template part. We don't have to `while ( have_posts() ) : the_post(); ... endwhile;`, keeping our code nice and DRY. template-parts/post-list.php ```php $posts = Post::all(); echo loop( $posts, 'template-parts/post-list-item' ); ``` Inside the loop, the global post object will be set to the current post in the loop. This allows you to use WordPress template tags such as `the_title()`, `the_content()`, and `the_ID()` without needing to call `setup_postdata()`. template-parts/post-list-item.php ```php

``` You can use `render_loop()` to echo the output of the loop directly: ```php render_loop( $posts, 'template-parts/post-list-item' ); ``` ##### `iterate()`[​](#iterate "Direct link to iterate") Iterate over a collection/array of arbitrary data. Each view is passed `index` and `item` as a the current item in the loop. ```php echo iterate( [ 1, 2, 3 ], 'template-parts/number-item' ); ``` template-parts/number-item.php ```php

Index:

Item:

``` You can use `render_iterate()` to echo the output of the iteration directly: ```php render_iterate( [ 1, 2, 3 ], 'template-parts/number-item' ); ``` #### View Shortcuts[​](#view-shortcuts "Direct link to View Shortcuts") When inside of a partial, you can prefix your path slug with `_` to load a sub-partial, appending everything after the `_` to the current partial's file name (with a dash separating them). template-parts/homepage/slideshow.php ```php
@foreach ( $slides as $slide ) @include( '_slide', [ 'text' => $slide->text ] ) @endforeach
``` template-parts/homepage/slideshow-slide.php ```php echo mantle_get_var( 'text', "Slide data!" ); ``` #### View Caching[​](#view-caching "Direct link to View Caching") Views can be cached using the `cache()` method. This will cache the view output for a specified duration. The cache will be stored in the WordPress object cache, allowing it to be shared across requests. ```php echo cache( 'template-parts/view', [ 'foo' => 'bar' ] )->cache(); // Cache for default of 15 minutes. echo cache( 'template-parts/view', [ 'foo' => 'bar' ] )->cache( HOUR_IN_SECONDS ); // Cache for a custom TTL. // You can also specify a cache key: echo cache( 'template-parts/view', [ 'foo' => 'bar' ] )->cache( ttl: HOUR_IN_SECONDS, key: 'my-custom-cache-key' ); ``` --- ### Basics #### [📄️ Requests and Routing](/docs/basics/requests.md) [Mantle provides a MVC framework on-top of WordPress. You can add a route fluently and send a response straight back without needing to work with WordPress's \`add\_rewrite\_rule()\` at all.](/docs/basics/requests.md) --- ### Features #### [📄️ Assets and Blocks](/docs/features/assets.md) [Mantle provides a fluent wrapper on-top of WordPress' enqueue system. It relies](/docs/features/assets.md) --- ### Assets and Blocks Mantle provides a fluent wrapper on-top of WordPress' enqueue system. It relies on the [wp-asset-manager](https://github.com/alleyinteractive/wp-asset-manager) package to provide a number of flexible methods for asset registration. Mantle includes a `Asset_Service_Provider` class for managing asset and block registration. Assets can be registered in any of your service providers, too. You can use the `Asset` facade or the `asset()` helper method to quickly access the API. info Mantle's front-end asset and block system aims to be as flexible as possible and inline with Alley's [create-wordpress-plugin](https://github.com/alleyinteractive/create-wordpress-plugin) project. #### Installation[​](#installation "Direct link to Installation") A fresh installation of Mantle will include a `package.json` and depends on **Node 16**. You can get started by running: ```bash npm install ``` Mantle is configured to have a development mode and a build mode to make it easy to develop and build your application. ```bash # Development mode npm run dev # Build mode npm run build ``` #### Configuration[​](#configuration "Direct link to Configuration") Mantle's build system is powered by Webpack and configured via the `webpack.config.js` file. Out of the box, you will be able to build standalone entries as well as WordPress blocks using either JavaScript or TypeScript. #### Standalone Entries[​](#standalone-entries "Direct link to Standalone Entries") Mantle is configured to read entries from the `entries` directory. You can create a new entry by creating a new folder in the `entries` directory and adding an `index.js`/`index.ts` file. For example, if you wanted to create an entry for your application's homepage, you could create a `entries/home` directory and add an `index.ts` file. Once you have created an entry, you can register and enqueue it via [Registering Assets](#registering-assets). What is a webpack entry? A webpack entry is a file that webpack will use to start building your application. It is the entry point to your application. For example, you could have an entry point for your application's homepage and another for articles. #### WordPress Blocks[​](#wordpress-blocks "Direct link to WordPress Blocks") Mantle is designed to make it easy to build WordPress blocks and works with Alley's standard practice of building and defining WordPress blocks. To get started, you can use Alley's Create Block package to define a new block: ```bash npx @alleyinteractive/create-block ``` Without providing any options the tool will prompt the user through several options for creating a block. Once the block is created, the block will be automatically registered and loaded by Mantle via the `Asset_Service_Provider`'s `load_blocks` method. #### Registering Assets[​](#registering-assets "Direct link to Registering Assets") Assets can be enqueued in a fluent-basis on top of the existing WordPress API using the `asset()` helper method. Mantle will read the Webpack manifest and automatically determine the URL for the asset. To get us started, let's enqueue a script from our `entries` directory: ```php asset()->script( '/example-entry/index.js' ); ``` Mantle will take it from there and register the asset with WordPress as well as all dependencies from `@wordpress/dependency-extraction-webpack-plugin`. Scripts can also be made to load with `async` and/or `defer`: ```php asset() ->script( '/example-entry/index.js' ) ->async() ->defer(); ``` Styles can be enqueued in the same way: ```php asset()->style( '/example-entry/index.css' ); ``` ##### Registering Non-Webpack Assets[​](#registering-non-webpack-assets "Direct link to Registering Non-Webpack Assets") Mantle also provides a way to register non-Webpack assets that don't come from the Webpack manifest. This is useful for registering assets from plugins or other sources. ```php asset()->script( 'example-handle', '/path/to/example.js' ); asset()->style( 'example-handle', '/path/to/example.css' ); ``` The version and other dependencies of the asset can also be specified with the `version()` and `dependencies()` method: ```php asset() ->script( 'example-handle', '/path/to/example.js' ) ->version( '1.0' ) ->dependencies( [ 'unicorn-js' ] ); ``` ##### Asset Conditions[​](#asset-conditions "Direct link to Asset Conditions") Mantle also provides a way to conditionally enqueue assets. By default, all assets registered will be enqueued on all pages. However, you can specify conditions for when the asset should be enqueued via Alley's `wp-asset-manager` plugin (which Mantle depends on). ```php asset() ->script( 'example-handle', '/path/to/example.js' ) ->condition( 'home' ); ``` For more information, checkout the [wp-asset-manager documentation](https://github.com/alleyinteractive/wp-asset-manager/#conditions). The `Asset_Service_Provider` included with Mantle has a \`on\_am\_asset\_conditions\`\` method that can be used to register/manipulate conditions. ```php namespace App\Providers; use Mantle\Assets\Asset_Service_Provider as Service_Provider; class Asset_Service_Provider extends Service_Provider { // ... /** * Filter the asset conditions for the site. * * @param array $conditions Conditions to filter. * @return array */ public function on_am_asset_conditions( array $conditions ): array { $conditions['podcast-page'] = is_singular( 'podcast' ) || is_post_type_archive( 'podcast' ); return $conditions; } } ``` We can now use the `podcast-page` condition in our asset registration: ```php asset() ->script( 'example-handle', '/path/to/example.js' ) ->condition( 'podcast-page' ); ``` --- ### Cache Mantle provides a fluent API for various caching back-ends. Internally is uses calls to [WordPress's object cache](https://developer.wordpress.org/reference/classes/wp_object_cache/). #### Cache Usage[​](#cache-usage "Direct link to Cache Usage") The cache cache instance can be retrieved using the `Mantle\Facade\Cache` facade, or by type-hinting the `Mantle\Contracts\Cache\Factory` contract for your class' dependencies. The cache repository does implement PSR-16. ##### Retrieving Data from the Cache[​](#retrieving-data-from-the-cache "Direct link to Retrieving Data from the Cache") The `get` method can be used to retrieve data from the cache. It supports a second argument to respond with a default value. Otherwise, it will return `null` if the cache key doesn't exist. ```php $value = Cache::get( 'key' ); $another = Cache::get( 'my-key', default: '123' ); ``` You can also use `get_multiple` to retrieve multiple cache keys at once. ```php $values = Cache::get_multiple( [ 'key1', 'key2' ] ); ``` ##### Checking for Item Existence[​](#checking-for-item-existence "Direct link to Checking for Item Existence") The `has` method can be used to check for a cache key's existence. ```php if ( Cache::has( 'key' ) ) { // ... } ``` ##### Storing Data in the Cache[​](#storing-data-in-the-cache "Direct link to Storing Data in the Cache") The `set` method can be used to store data in the cache. By default it will be stored indefinitely unless `$seconds` is passed to specify the cache duration. ```php Cache::set( 'key', 'value', $seconds ); ``` You can also pass a `DateTimeInterface` object to specify the expiration time. ```php Cache::set( 'key', 'value', now()->addMinutes( 10 ) ); Cache::set( 'key', 'value', new DateTime( '2022-01-01' ) ); ``` The `set_multiple` method can be used to store multiple cache keys at once. ```php Cache::set_multiple( [ 'key1' => 'value1', 'key2' => 'value2' ], $ttl ); ``` The `remember` method can be used to store data in the cache and pass a closure to set a default value if the cache item does not exist. ```php Cache::remember( 'key', $seconds, function() { return 'the expensive function'; } ); ``` The `remember_forever` method can be used to store data in the cache indefinitely. ```php Cache::remember_forever( 'key', function() { return 'the expensive function'; } ); ``` ##### Incrementing / Decrementing Values[​](#incrementing--decrementing-values "Direct link to Incrementing / Decrementing Values") The increment and decrement methods may be used to adjust the value of integer items in the cache. Both of these methods accept an optional second argument indicating the amount by which to increment or decrement the item's value: ```php Cache::increment( 'key' ); Cache::increment( 'key', $amount ); Cache::decrement( 'key' ); Cache::decrement( 'key', $amount ); ``` tip The data should already be stored as an integer in the cache. ##### Deleting Items from the Cache[​](#deleting-items-from-the-cache "Direct link to Deleting Items from the Cache") The `delete` method can be used to remove an item from the cache: ```php Cache::delete( 'key' ); ``` The `delete_multiple` method can be used to remove multiple items from the cache: ```php Cache::delete_multiple( [ 'key1', 'key2' ] ); ``` ##### Helpers[​](#helpers "Direct link to Helpers") The cache API includes a `cache()` helper which can be used to store and retrieve data via the cache. When the `cache` function is called with a single string argument it will return the value of the given cache key. ```php $value = cache( 'key-to-get' ); ``` If you provide an array of key / value pairs and an expiration time to the function, it will store values in the cache for the specified duration: ```php cache( [ 'key' => 'value' ], $seconds ); ``` When the cache function is called without any arguments, it returns an instance of the `Mantle\Contracts\Cache\Factory` implementation, allowing you to call other caching methods: ```php cache()->remember( 'posts', $seconds, function() { return Posts::popular()->get(); } ); ``` #### Stale While Revalidate (SWR)[​](#stale-while-revalidate-swr "Direct link to Stale While Revalidate (SWR)") Commonly used in React, Stale While Revalidate (SWR) is a caching strategy that allows you to show stale data while fetching new data in the background. This can be useful for improving the perceived performance of your application. The application will use stale data for a short period of time while fetching new data in the background. The user does not have to wait for the new data to be fetched before seeing the page. Using SWR with Mantle, you can use the `flexible` or `swr` methods to retrieve data from the cache and reuse it for a period of stale time before it is updated in the background. The cache will return the stale data and then update the cache with the new data after the response is sent to the user. ```php // The cache will return the stale data for 1 hour before // updating the cache. The stale data will be used for // 1 day before it is removed from the cache. $data = Cache::flexible( key: 'cache-key', stale: now()->addHour(), expire: now()->addDay(), callback: function() { return 'the expensive function'; }, ); ``` #### Cache Tags[​](#cache-tags "Direct link to Cache Tags") Cache providers can support adding tags to a cache key to allow for simpler cache keys. ```php Cache::tags( [ 'users' ] )->get( $user_id ); Cache::tags( 'posts' )->get( $post_id ); ``` The tags method will return a cache factory allowing you the ability to store child cache keys in the same interface as the cache API. ```php Cache::tags( [ 'users' ] )->put( 'hello', $world, $seconds ); Cache::tags( [ 'users' ] )->remember( 'name', $seconds, function() { return 'smith'; } ); ``` Cache tags can use any normal cache method, such as `get`, `put`, `remember`, `delete`, `increment`, `decrement`, and `flexible`. ```php Cache::tags( [ 'users' ] )->delete( 'hello' ); Cache::tags( 'example' )->flexible( key: 'cache-key', stale: now()->addHour(), expire: now()->addDay(), callback: fn () => wp_remote_get( 'https://example.com' ), ); ``` --- ### File System Mantle includes the [Flysystem](https://github.com/thephpleague/flysystem) package to power an abstract interface to various local and remote filesystems. Files can exist local on the disk (inside the normal `wp-content/uploads` folder), on a FTP server, or in a S3 bucket. The API remains the same for all drivers, allowing a fluent interface to storing a file in a remote bucket with only a few lines of configuration. #### Configuration[​](#configuration "Direct link to Configuration") The configuration for the filesystem lives in `config/filesystem.php`. This file will contain the "disks" that you can access as well as the "disk's" driver. For example, a application can have multiple S3 disks that all use the 's3' driver. The disk can reference a different bucket set of configuration for the same driver. You may configure as many disks as you like, and may even have multiple disks that use the same driver. #### Local Storage[​](#local-storage "Direct link to Local Storage") By default, the application will use the local storage disk/driver. This will store files in WordPress' upload directory. Storage of files here will always be assumed to be public. ```php Storage::put( 'file.txt', 'Contents' ); Storage::drive( 'local' )->put( 'file.txt', 'Contents' ); ``` #### Remote Storage[​](#remote-storage "Direct link to Remote Storage") The filesystem can store files with any adapter that Flysystem supports. Out of the box, Mantle supports the FTP and S3 adapter. ##### Composer Dependency[​](#composer-dependency "Direct link to Composer Dependency") Before using the SFTP or S3 drivers, you will need to install the appropriate package via Composer: * SFTP: `league/flysystem-sftp ~1.0` * Amazon S3: `league/flysystem-aws-s3-v3 ~1.0` ##### Configuration[​](#configuration-1 "Direct link to Configuration") The S3/FTP configuration is located int he `config/filesystem.php` file. To use the S3/FTP disk you need to fill in your own S3/FTP configuration and credentials. #### Caching[​](#caching "Direct link to Caching") To enable caching for a given disk, you may add a cache directive to the disk's configuration options. The cache option should be an array of caching options containing the disk name, the expire time in seconds, and the cache prefix: ```php 's3' => [ 'driver' => 's3', // Other Disk Options... 'cache' => [ 'store' => 'memcached', 'expire' => 600, 'prefix' => 'cache-prefix', ], ], ``` #### Obtaining Disk Instances[​](#obtaining-disk-instances "Direct link to Obtaining Disk Instances") The `Storage` facade may be used to interact with any of your configured disks. For example, you may use the put method on the facade to store an avatar on the default disk. If you call methods on the Storage facade without first calling the disk method, the method call will automatically be passed to the default disk: ```php use Mantle\Facade\Storage; Storage::put( 'avatars/1', $file_contents ); ``` If your application interacts with multiple disks, you may use the disk method on the `Storage` facade to work with files on a particular disk: ```php Storage::drive( 's3' )->put( 'avatars/1', $file_contents ); ``` #### Storing Files[​](#storing-files "Direct link to Storing Files") Files can be stored using the `put()` method by passing either file contents or a file stream. ```php use Mantle\Facade\Storage; Storage::put( 'avatars/1', $file_contents ); ``` You can upload a local file with the `put_file()` method. Use the `put_file_as` to specify a name. ```php use Mantle\Facade\Storage; Storage::put_file( '/path/to/store', '/var/local.jpg' ); ``` ##### File Uploads[​](#file-uploads "Direct link to File Uploads") Files can be uploaded directly from HTTP requests to any storage disk. ```php namespace App\Http\Controller; use Mantle\Http\Controller; use Mantle\Http\Request; class Photo_Controller extends Controller { public function __invoke( Request $request ) { $path = $request->file( 'avatar' )->store( 'disk-name' ); return $path; } } ``` You can also use the `Storage` facade directly. ```php use Mantle\Facade\Storage; Storage::put_file( 'avatars', $request->file( 'avatar' ) ); ``` ##### File Visibility[​](#file-visibility "Direct link to File Visibility") By default files will assume to be stored publicly. All local files will be stored publicly out of the box, too. You can use a remote disk such as S3 to store a file privately. tip To add a private local disk, add a disk and specify a path to a private folder. ```php use Mantle\Facade\Storage; Storage::put( 'file.jpg', $contents, 'public' ); ``` You can store a private file by changing the third argument. ```php use Mantle\Facade\Storage; Storage::disk( 's3' )->put( 'file.jpg', $contents, 'private' ); Storage::disk( 's3' )->put( 'file.jpg', $contents, [ 'visibility' => 'private' ] ); ``` #### Retrieving Files[​](#retrieving-files "Direct link to Retrieving Files") Files can be retrieved using the `Storage` facade. ```php use Mantle\Facade\Storage; $path = Storage::url( '/path/to/file.jpg' ); ``` ##### Retrieving Private Files[​](#retrieving-private-files "Direct link to Retrieving Private Files") Private files commonly need to use a temporary URL to retrieve the file. For a private file on S3, Mantle can generate a temporary S3 URL. ```php $url = Storage::disk( 's3' )->temporary_url( '/path/to/file.jpg' ); ``` --- ### Hooks and Events WordPress' hooks are the most flexible way of integrating with WordPress. Mantle aims to make these a bit more flexible for the 21st century. Mantle provides a simple observer pattern implementation, allowing you to listen for various events that occur within WordPress and Mantle. Events can be your traditional WordPress hooks (`pre_get_posts`, `init`, etc.) or event objects: standalone classes used to define a specific event. This provides a great way to decouple various aspects of your application. One example would be an 'order shipped' event. An event called `App\Events\Order_Shipped` is instantiated and dispatched whenever an order is shipped. Underneath, this utilizes the traditional WordPress hook system with an action fired using the class' full name. This flexible organization of WordPress events as well as [safety guards on top of type-hints](#using-hooks-safely-with-type-hint-declarations) allows you to have the most flexibility possible when managing events in your application. #### Registering Events & Listeners[​](#registering-events--listeners "Direct link to Registering Events & Listeners") The `App\Providers\Event_Service_Provider` included with Mantle provides a convenient place to register all your application's event listeners. The `listen` property contains an array of all events (keys) and their listeners (values). Here's an example for an `Order_Shipped` event: ```php use App\Events\Order_Shipped; use App\Listeners\Send_Ship_Notification; use App\Listeners\Modify_Orders_Page; /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Order_Shipped::class => [ Send_Ship_Notification::class, ], 'pre_get_posts' => [ Modify_Orders_Page::class, ], ]; ``` ##### Generating Events and Listeners[​](#generating-events-and-listeners "Direct link to Generating Events and Listeners") Events can be manually written or generated for you: ```text wp mantle make:event ``` Listeners can also be automatically generated for you: ```text wp mantle make:listener [] ``` The `make:listener` command supports passing a specific event to automatically listen for. ##### Manually Registering Events[​](#manually-registering-events "Direct link to Manually Registering Events") Event listeners can be registered via the `Event_Service_Provider` or by individual service providers. They can also register listeners on a class or closure basis: ```php /** * Register any other events for your application. * * @return void */ public function boot() { Event::listen( 'init', function () { // ... }); } ``` ##### Event Discovery[​](#event-discovery "Direct link to Event Discovery") Instead of registering events and listeners manually in the `Event_Service_Provider` Mantle can automatically discover listeners in your application. When discovery is enabled (it is by default), Mantle will automatically find and discover event listeners by scanning your application's `listeners` directory. Here's an example of a listener that will automatically listen for an `Order_Shipped` event: ```php use App\Events\Order_Shipped; class Order_Listener { /** * Handle the event. * * @param Order_Shipped $event * @return void */ public function handle( Order_Shipped $event ) { // } } ``` You can also listen for multiple events in the same listener and specify priority using the `_at_%PRIORITY%` syntax: ```php use App\Events\Order_Shipped; use App\Events\Order_Received; use App\Events\Order_Cancellation; class Order_Listener { /** * Handle the event. * * @param Order_Shipped $event * @return void */ public function handle( Order_Shipped $event ) { // } /** * Handle the event. * * @param Order_Received $event * @return void */ public function handle_order_received( Order_Received $event ) { // } /** * Handle the event at 20 priority. * * @param Order_Cancellation $event * @return void */ public function handle_handle_cancellation_at_20( Order_Cancellation $event ) { // } } ``` Listeners can also automatically listen for WordPress events using PHP Attributes or naming the function after the action using the `on_{hook}` function name: ```php use Mantle\Support\Attributes\Action; use WP_Query; class Customer_Listener { /** * Handle the callback to the 'attribute-based' action. */ #[Action('attribute-based')] public function handle_special_action_callback( $event ) { // ... } /** * Handle the callback to the 'attribute-based' action at 20 priority. */ #[Action('attribute-based', 20)] public function handle_special_action_callback_20( $event ) { // ... } /** * Handle the query. * * @param WP_Query $event * @return void */ public function on_pre_get_posts( WP_Query $query ) { // } /** * Handle the redirect. * * @param mixed redirect * @return void */ public function on_parse_redirect_at_20( $redirect ) { // } } ``` ###### Caching Discovery[​](#caching-discovery "Direct link to Caching Discovery") Event discovery is enabled by default and can be disabled via the `should_discover_events()` method in `Event_Service_Provider`. For performance, it is ideal to cache the events that are discovered since the Reflection service is used. To cache the events, run the following command on deployment: wp mantle event:cache #### Using Hooks Safely with Type-hint Declarations[​](#using-hooks-safely-with-type-hint-declarations "Direct link to Using Hooks Safely with Type-hint Declarations") WordPress' actions/filters are extended by Mantle to allow for safe use of type/return declarations with a fatal error. Mantle provides a wrapper on top of `add_action()` and `add_filter()` (these can be used interchangeable with `Event::listen( ... )`). ##### Problem With Type Declarations in Core[​](#problem-with-type-declarations-in-core "Direct link to Problem With Type Declarations in Core") Here's an example of a hook with a type-hint that will throw a fatal error (and potentially bring down your whole site!): ```php use WP_Post; add_filter( 'my_custom_filter', function ( array $posts ): array { return array_map( fn ( WP_Post $post ) => $post->ID, $posts ); }, ); // Elsewhere in your application a plugin adds this filter at a slightly higher priority: add_filter( 'my_custom_filter', function ( array $posts ): array { if ( another_function() ) { return null; } return $posts; }, 8, ); // Run the filter and get a fatal error. $posts = apply_filters( 'my_custom_filter', $posts ); ``` The above example throws a fatal error because the type declaration `array` is defined on your first callback. The second callback can return `null` which is not an array. Once WordPress reaches the first callback you'll be met with a fatal error that `$posts` is not an array. #### Wrapper on Core Actions/Filters[​](#wrapper-on-core-actionsfilters "Direct link to Wrapper on Core Actions/Filters") To alleviate the above problem from happening, Mantle provides a wrapper on the callback function for actions/filters. The wrapper will ensure that the type-hint on the callback matches what is being passed to it. In the above example, Mantle would have caught that `$posts` is not an array and would have converted it to an array before invoking the type-hinted callback. Here's an example of a safe type-hinted callback: ```php use WP_Query; use function Mantle\Framework\Helpers\add_action; add_action( 'pre_get_posts', function( WP_Query $query ) { // $query will always be an instance of WP_Query. } ); ``` ```php use Mantle\Support\Collection; use function Mantle\Framework\Helpers\add_filter; add_filter( 'the_posts', function( array $posts ) { // $posts will always be an array. } ); // Also supports translating between a Arrayable and an array. add_filter( 'the_posts', function( Collection $posts ) { // $posts will always be a Collection. return $posts->to_array(); } ); apply_filters( 'the_filter_to_apply', [ 1, 2, 3 ] ); ``` Mantle's wrapper on top of actions/filters can be accessed by using these helper methods: * `Mantle\Framework\Helpers\add_action()` * `Mantle\Framework\Helpers\add_filter()` * `Mantle\Facade\Event::listen()` * `Event::listen()` ##### Handling Type Declaration Being Translated[​](#handling-type-declaration-being-translated "Direct link to Handling Type Declaration Being Translated") By default, Mantle will attempt to translate type declarations without any need for customization to prevent errors. For example, an action is listened for that expects a `string` and has a `int` passed. Mantle will properly translate it to a `string`. Type declaring a `Collection` will have an array automatically translate to a collection and so on. For the edge cases where a method type declares an object and needs manual intervention, Mantle will fire a filter that will allow the translation to be properly handled: ```php add_filter( 'mantle-typehint-resolve:Custom_Class', function ( $param ) { return new Custom_Class( $param ); } ); ``` #### Find a Hook's Usage in the Codebase[​](#find-a-hooks-usage-in-the-codebase "Direct link to Find a Hook's Usage in the Codebase") Quickly calculate the usage of a specific WordPress hook throughout your code base. Mantle will read all specified files in a specific path to find all uses of a specific action/filter along with their respective line number. On initial scan of the file system, the results can be a bit slow to build a cache of all files on the site. By default Mantle will ignore all `test` and `vendor/` files. The default search path is the `wp-content/` folder of your installation. ```text wp mantle hook-usage [--search-path] [--format] ``` --- ### HTTP Client #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides a fluent and expressive wrapper around the [WordPress HTTP API](https://developer.wordpress.org/plugins/http-api/) allowing you to quickly make HTTP requests with external services. The HTTP Client combined with support for [testing and faking Remote Requests](/docs/testing/remote-requests.md) provides you a full solution for making and testing external HTTP requests end-to-end. #### Making Requests[​](#making-requests "Direct link to Making Requests") The HTTP Client is installed by default with Mantle. It also supports being used outside of the framework. * Framework Use * Standalone Use The Http Client is included with Mantle out of the box. Making requests with the HTTP Client can be done using any HTTP verb. Requests can be made using the `Http` facade class or by instantiating `Mantle\Http_Client\Http_Client` directly: ```php use Mantle\Facade\Http; $response = Http::get( 'https://example.org/' ); ``` You can also use other HTTP methods fluently on the `Http` facade: ```php use Mantle\Facade\Http; $response = Http::post( 'https://example.org/', [ 'name' => 'Mantle', ] ); ``` Install the Http Client package via Composer: ```bash composer require mantle-framework/http-client ``` Making requests with the HTTP Client can be done using any HTTP verb. Requests can be made by instantiating `Mantle\Http_Client\Factory` directly: ```php use Mantle\Http_Client\Factory; $http = new Factory(); $response = $http->get( 'https://example.org/' ); // Or using a fluent interface: $response = $http ->timeout( 10 ) ->get( 'https://example.org/' ); ``` ##### Request Data[​](#request-data "Direct link to Request Data") Many HTTP requests include some sort of a payload for `POST`, `PUT`, `PATCH`, and `DELETE` methods. These methods access an array of data as their second argument. By default these will be sent using JSON: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; Http::post( 'http://example.org/endpoint', [ 'name' => 'Mantle', ] ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->post( 'http://example.org/endpoint', [ 'name' => 'Mantle', ] ); ``` ###### Sending a Body[​](#sending-a-body "Direct link to Sending a Body") Requests can also be made using a form body or a raw body: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; Http::as_form()->post( 'http://example.org/endpoint', [ 'name' => 'Mantle', ] ); Http::with_body( 'raw-body' )->post( 'http://example.org/endpoint' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->as_form()->post( 'http://example.org/endpoint', [ 'name' => 'Mantle', ] ); Factory::create()->with_body( 'raw-body' )->post( 'http://example.org/endpoint' ); ``` ###### GET Request Query Parameters[​](#get-request-query-parameters "Direct link to GET Request Query Parameters") When making `GET` requests, you can either pass a URL with a query string directly or pass an array of key / value pairs as the second argument: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // The actual URL will be https://example.org/query?name=mantle Http::get( 'https://example.org/query', [ 'name' => 'mantle', ] ); ``` ```php use Mantle\Http_Client\Factory; // The actual URL will be https://example.org/query?name=mantle Factory::create()->get( 'https://example.org/query', [ 'name' => 'mantle', ] ); ``` ##### Headers[​](#headers "Direct link to Headers") Headers can be added to a request using the `with_header()` and `with_headers()` methods: * Framework Use * Standalone Use ```php Http::with_headers( [ 'X-Api-Key' => 'password', ] )->post( 'https://example.org' ); Http::with_header( 'X-Special-Header', 'value' )->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->with_headers( [ 'X-Api-Key' => 'password', ] )->post( 'https://example.org' ); Factory::create()->with_header( 'X-Special-Header', 'value' )->post( 'https://example.org' ); ``` The `accept` method can be used to specify the content type that your application is expecting in response: * Framework Use * Standalone Use ```php Http::accept( 'text/plain' )->post( 'https://example.org' ); Http::accept_json()->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->accept( 'text/plain' )->post( 'https://example.org' ); Factory::create()->accept_json()->post( 'https://example.org' ); ``` ##### Authentication[​](#authentication "Direct link to Authentication") You can specify basic authentication and bearer token credentials with built-in helper methods: * Framework Use * Standalone Use ```php Http::with_basic_auth( 'username', 'password' )->post( 'https://example.org' ); // Passed as Authorization: Bearer Http::with_token( '' )->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->with_basic_auth( 'username', 'password' )->post( 'https://example.org' ); // Passed as Authorization: Bearer Factory::create()->with_token( '' )->post( 'https://example.org' ); ``` ##### Timeout[​](#timeout "Direct link to Timeout") The `timeout` method may be used to specify a maximum number of seconds to wait for a response. The default is 5 seconds: * Framework Use * Standalone Use ```php Http::timeout( 10 )->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->timeout( 10 )->post( 'https://example.org' ); ``` ##### Retries[​](#retries "Direct link to Retries") If you would like HTTP client to automatically retry the request if a client or server error occurs, you may use the `retry` method. The `retry` method accepts the maximum number of times the request should be attempted and the number of milliseconds that Mantle should wait in between attempts: * Framework Use * Standalone Use ```php Http::retry( 3 )->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->retry( 3 )->post( 'https://example.org' ); ``` ##### Error Handling[​](#error-handling "Direct link to Error Handling") By default, errors will be handled and converted into a `Mantle\Http_Client\Response` object. This includes `WP_Error` objects that are returned by WordPress. No exceptions are thrown by default. Optionally, you can opt for an exception to be thrown if an error occurs: * Framework Use * Standalone Use ```php try { Http::throw_exception()->post( 'https://example.org' ); } catch ( \Mantle\Http_Client\Http_Client_Exception $e ) { // Handle the exception. } ``` ```php use Mantle\Http_Client\Factory; try { Factory::create()->throw_exception()->post( 'https://example.org' ); } catch ( \Mantle\Http_Client\Http_Client_Exception $e ) { // Handle the exception. } ``` ##### Available Request Methods[​](#available-request-methods "Direct link to Available Request Methods") The HTTP Client provides a comprehensive set of methods for configuring and making HTTP requests. Here's a complete reference with examples for each method: * [URL and Method Configuration](#url-and-method-configuration) * [Base URL Management](#base-url-management) * [URL Management](#url-management) * [HTTP Method Configuration](#http-method-configuration) * [Content Type and Headers](#content-type-and-headers) * [Content Type Configuration](#content-type-configuration) * [Accept Headers](#accept-headers) * [Header Management](#header-management) * [Basic Authentication](#basic-authentication) * [Bearer Token Authentication](#bearer-token-authentication) * [Cookie Management](#cookie-management) * [Request Body Methods](#request-body-methods) * [Request Options](#request-options) * [Redirect Handling](#redirect-handling) * [Middleware Management](#middleware-management) * [Streaming Methods](#streaming-methods) * [Retry Configuration](#retry-configuration) * [Exception Handling](#exception-handling) * [HTTP Request Methods](#http-request-methods) ###### URL and Method Configuration[​](#url-and-method-configuration "Direct link to URL and Method Configuration") ###### Base URL Management[​](#base-url-management "Direct link to Base URL Management") Set a base URL that will be prepended to all subsequent requests using `set_base_url()` and retrieve it with `get_base_url()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Set a base URL for multiple requests $client = Http::set_base_url( 'https://api.example.com/v1' ); // Now all requests will use this base URL $response = $client->get( '/users' ); // Requests https://api.example.com/v1/users // Get the current base URL $base_url = $client->get_base_url(); // Returns: https://api.example.com/v1 ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Set a base URL for multiple requests $client = $http->set_base_url( 'https://api.example.com/v1' ); // Now all requests will use this base URL $response = $client->get( '/users' ); // Requests https://api.example.com/v1/users // Get the current base URL $base_url = $client->get_base_url(); // Returns: https://api.example.com/v1 ``` ###### URL Management[​](#url-management "Direct link to URL Management") Configure the full URL for a request using `set_url()` and retrieve it with `get_url()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Set the complete URL $client = Http::set_url( 'https://httpbin.org/get' ); // Get the configured URL $url = $client->get_url(); // Returns: https://httpbin.org/get ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Set the complete URL $client = $http->set_url( 'https://httpbin.org/get' ); // Get the configured URL $url = $client->get_url(); // Returns: https://httpbin.org/get ``` ###### HTTP Method Configuration[​](#http-method-configuration "Direct link to HTTP Method Configuration") Set the HTTP method for the request using `set_method()` and retrieve it with `get_method()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; use Mantle\Http_Client\Http_Method; // Set method using enum $client = Http::set_method( Http_Method::POST ); // Set method using string $client = Http::set_method( 'PATCH' ); // Get the current method $method = $client->get_method(); // Returns Http_Method enum ``` ```php use Mantle\Http_Client\Factory; use Mantle\Http_Client\Http_Method; $http = new Factory(); // Set method using enum $client = $http->set_method( Http_Method::POST ); // Set method using string $client = $http->set_method( 'PATCH' ); // Get the current method $method = $client->get_method(); // Returns Http_Method enum ``` ###### Content Type and Headers[​](#content-type-and-headers "Direct link to Content Type and Headers") ###### Content Type Configuration[​](#content-type-configuration "Direct link to Content Type Configuration") Configure how request data should be sent using `as_form()`, `as_json()`, and `content_type()` methods: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Send data as form-encoded (application/x-www-form-urlencoded) Http::as_form()->post( 'https://example.org', [ 'name' => 'John Doe', 'email' => 'john@example.com', ] ); // Send data as JSON (application/json) - this is the default Http::as_json()->post( 'https://example.org', [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], ] ); // Set a custom content type Http::content_type( 'application/xml' )->post( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Send data as form-encoded (application/x-www-form-urlencoded) $http->as_form()->post( 'https://example.org', [ 'name' => 'John Doe', 'email' => 'john@example.com', ] ); // Send data as JSON (application/json) - this is the default $http->as_json()->post( 'https://example.org', [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], ] ); // Set a custom content type $http->content_type( 'application/xml' )->post( 'https://example.org' ); ``` ###### Accept Headers[​](#accept-headers "Direct link to Accept Headers") Specify what content type you expect in the response using `accept_json()` and `accept()` methods: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Accept JSON responses Http::accept_json()->get( 'https://api.example.com/data' ); // Accept specific content type Http::accept( 'text/xml' )->get( 'https://api.example.com/xml-data' ); // Accept multiple content types Http::accept( 'application/json, text/plain' )->get( 'https://api.example.com/data' ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Accept JSON responses $http->accept_json()->get( 'https://api.example.com/data' ); // Accept specific content type $http->accept( 'text/xml' )->get( 'https://api.example.com/xml-data' ); // Accept multiple content types $http->accept( 'application/json, text/plain' )->get( 'https://api.example.com/data' ); ``` ###### Header Management[​](#header-management "Direct link to Header Management") Add and manage HTTP headers using `with_headers()`, `with_header()`, `headers()`, `header()`, `clear_headers()`, and `with_user_agent()` methods: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Add multiple headers at once Http::with_headers( [ 'X-API-Key' => 'your-api-key', 'X-Client-Version' => '1.0.0', 'Accept-Language' => 'en-US', ] )->get( 'https://api.example.com/data' ); // Add a single header Http::with_header( 'X-Custom-Header', 'custom-value' )->get( 'https://example.org' ); // Replace an existing header (default behavior) Http::with_header( 'User-Agent', 'My Custom Agent', true )->get( 'https://example.org' ); // Get all current headers $headers = Http::with_header( 'X-Test', 'value' )->headers(); // Returns: ['X-Test' => 'value', ...] // Get a specific header value $value = Http::with_header( 'X-Test', 'value' )->header( 'X-Test' ); // Returns: 'value' // Clear all headers Http::with_header( 'X-Test', 'value' )->clear_headers()->get( 'https://example.org' ); // Set a custom user agent Http::with_user_agent( 'MyApp/1.0 (https://myapp.com)' )->get( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Add multiple headers at once $http->with_headers( [ 'X-API-Key' => 'your-api-key', 'X-Client-Version' => '1.0.0', 'Accept-Language' => 'en-US', ] )->get( 'https://api.example.com/data' ); // Add a single header $http->with_header( 'X-Custom-Header', 'custom-value' )->get( 'https://example.org' ); // Replace an existing header (default behavior) $http->with_header( 'User-Agent', 'My Custom Agent', true )->get( 'https://example.org' ); // Get all current headers $headers = $http->with_header( 'X-Test', 'value' )->headers(); // Returns: ['X-Test' => 'value', ...] // Get a specific header value $value = $http->with_header( 'X-Test', 'value' )->header( 'X-Test' ); // Returns: 'value' // Clear all headers $http->with_header( 'X-Test', 'value' )->clear_headers()->get( 'https://example.org' ); // Set a custom user agent $http->with_user_agent( 'MyApp/1.0 (https://myapp.com)' )->get( 'https://example.org' ); ``` ###### Authentication[​](#authentication-1 "Direct link to Authentication") ###### Basic Authentication[​](#basic-authentication "Direct link to Basic Authentication") Use HTTP Basic Authentication with username and password using the `with_basic_auth()` method: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Basic auth with username and password Http::with_basic_auth( 'username', 'password' )->get( 'https://api.example.com/protected' ); // Example with a real API Http::with_basic_auth( 'api_user', 'secret123' ) ->get( 'https://api.github.com/user/repos' ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Basic auth with username and password $http->with_basic_auth( 'username', 'password' )->get( 'https://api.example.com/protected' ); // Example with a real API $http->with_basic_auth( 'api_user', 'secret123' ) ->get( 'https://api.github.com/user/repos' ); ``` ###### Bearer Token Authentication[​](#bearer-token-authentication "Direct link to Bearer Token Authentication") Use Bearer token authentication using the `with_token()` method: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Bearer token (default type) Http::with_token( 'your-access-token' )->get( 'https://api.example.com/user' ); // Custom token type Http::with_token( 'your-api-key', 'API-Key' )->get( 'https://api.example.com/data' ); // Sends: Authorization: API-Key your-api-key // Example with GitHub API Http::with_token( 'ghp_xxxxxxxxxxxx' ) ->get( 'https://api.github.com/user' ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Bearer token (default type) $http->with_token( 'your-access-token' )->get( 'https://api.example.com/user' ); // Custom token type $http->with_token( 'your-api-key', 'API-Key' )->get( 'https://api.example.com/data' ); // Sends: Authorization: API-Key your-api-key // Example with GitHub API $http->with_token( 'ghp_xxxxxxxxxxxx' ) ->get( 'https://api.github.com/user' ); ``` ###### Cookie Management[​](#cookie-management "Direct link to Cookie Management") Handle cookies for stateful requests using `clear_cookies()`, `with_cookies()`, and `with_cookie()` methods: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Set multiple cookies at once Http::with_cookies( [ 'session_id' => 'abc123', 'preference' => 'dark_mode', 'language' => 'en', ] )->get( 'https://example.org' ); // Add a single cookie using WP_Http_Cookie $cookie = new \WP_Http_Cookie( [ 'name' => 'user_session', 'value' => 'xyz789', 'domain' => '.example.com', 'path' => '/', ] ); Http::with_cookie( $cookie )->get( 'https://api.example.com/user' ); // Clear all cookies before making a request Http::with_cookies( [ 'old_session' => 'previous_value', 'temp_data' => 'some_data', ] ) ->clear_cookies() ->get( 'https://example.org' ); // Request made without any cookies ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Set multiple cookies at once $http->with_cookies( [ 'session_id' => 'abc123', 'preference' => 'dark_mode', 'language' => 'en', ] )->get( 'https://example.org' ); // Add a single cookie using WP_Http_Cookie $cookie = new \WP_Http_Cookie( [ 'name' => 'user_session', 'value' => 'xyz789', 'domain' => '.example.com', 'path' => '/', ] ); $http->with_cookie( $cookie )->get( 'https://api.example.com/user' ); // Clear all cookies before making a request $http->with_cookies( [ 'old_session' => 'previous_value', 'temp_data' => 'some_data', ] ) ->clear_cookies() ->get( 'https://example.org' ); // Request made without any cookies ``` ###### Request Body Methods[​](#request-body-methods "Direct link to Request Body Methods") Configure the request body content using `with_body()`, `with_json()`, `body()`, and `body_format()` methods: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Send raw body with content type Http::with_body( '{"custom": "json", "data": true}', 'application/json' )->post( 'https://api.example.com/webhook' ); // Send XML data Http::with_body( 'John', 'application/xml' )->post( 'https://api.example.com/users' ); // Send JSON data (alternative to as_json) Http::with_json( [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], ] )->post( 'https://api.example.com/users' ); // Get the current body content $body = Http::with_json( ['test' => 'data'] )->body(); // Set body format Http::body_format( 'form' )->post( 'https://example.org', [ 'name' => 'John', ] ); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Send raw body with content type $http->with_body( '{"custom": "json", "data": true}', 'application/json' )->post( 'https://api.example.com/webhook' ); // Send XML data $http->with_body( 'John', 'application/xml' )->post( 'https://api.example.com/users' ); // Send JSON data (alternative to as_json) $http->with_json( [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], ] )->post( 'https://api.example.com/users' ); // Get the current body content $body = $http->with_json( ['test' => 'data'] )->body(); // Set body format $http->body_format( 'form' )->post( 'https://example.org', [ 'name' => 'John', ] ); ``` ###### Request Options[​](#request-options "Direct link to Request Options") Configure low-level request options using `with_options()`, `without_verifying()`, and `timeout()` methods: ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Add custom options (merged with existing) $http->with_options( [ 'sslverify' => false, 'timeout' => 30, 'user-agent' => 'Custom Agent', ], true )->get( 'https://example.org' ); // Replace all options $http->with_options( [ 'timeout' => 60, ], false )->get( 'https://example.org' ); // Disable SSL verification (useful for development) $http->without_verifying()->get( 'https://self-signed.badssl.com/' ); // Set request timeout $http->timeout( 30 )->get( 'https://slow-api.example.com/data' ); ``` ###### Redirect Handling[​](#redirect-handling "Direct link to Redirect Handling") Control how redirects are handled using `without_redirecting()` and `with_redirecting()` methods: ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Disable automatic redirect following $http->without_redirecting()->get( 'https://httpbin.org/redirect/1' ); // Will return the 3xx response instead of following the redirect // Enable redirects with custom limit $http->with_redirecting( 10 )->get( 'https://httpbin.org/redirect/5' ); // Will follow up to 10 redirects // Default redirect behavior (5 redirects) $http->with_redirecting()->get( 'https://httpbin.org/redirect/3' ); ``` ###### Middleware Management[​](#middleware-management "Direct link to Middleware Management") Add custom middleware to modify requests and responses using `middleware()`, `prepend_middleware()`, `get_middleware()`, `without_middleware()`, and `filter_middleware()` methods: ```php use Mantle\Http_Client\Factory; use Mantle\Http_Client\Http_Client; use Closure; $http = new Factory(); // Add middleware to log requests $http->middleware( function ( Http_Client $client, Closure $next ) { error_log( 'Making request to: ' . $client->get_url() ); $response = $next( $client ); error_log( 'Response status: ' . $response->status() ); return $response; } )->get( 'https://httpbin.org/get' ); // Add middleware to the beginning of the stack $http->prepend_middleware( function ( Http_Client $client, Closure $next ) { // This runs before other middleware $client->with_header( 'X-Request-ID', uniqid() ); return $next( $client ); } )->get( 'https://api.example.com/data' ); // Get all registered middleware $middleware = $http->middleware( $some_middleware )->get_middleware(); // Remove specific middleware $http->middleware( $middleware1 ) ->middleware( $middleware2 ) ->without_middleware( 'middleware_name' ) ->get( 'https://example.org' ); // Filter middleware based on a callback $http->middleware( $middleware1 ) ->middleware( $middleware2 ) ->filter_middleware( function ( $middleware ) { return $middleware !== $unwanted_middleware; } ) ->get( 'https://example.org' ); ``` ###### Streaming Methods[​](#streaming-methods "Direct link to Streaming Methods") Handle file downloads and streaming responses using `stream()` and `dont_stream()` methods: ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Stream response to a file $http->stream( '/path/to/download/file.zip' ) ->get( 'https://example.com/large-file.zip' ); // Stream response without saving to file (for processing) $http->stream()->get( 'https://api.example.com/large-dataset.json' ); // Disable streaming (default behavior) $http->stream( '/tmp/file.zip' ) ->dont_stream() ->get( 'https://example.com/file.zip' ); ``` ###### Retry Configuration[​](#retry-configuration "Direct link to Retry Configuration") Configure automatic request retries using the `retry()` method: ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Retry up to 3 times with no delay $http->retry( 3 )->get( 'https://unreliable-api.example.com/data' ); // Retry up to 5 times with 1000ms delay between attempts $http->retry( 5, 1000 )->post( 'https://api.example.com/webhook', [ 'event' => 'user_signup', ] ); // Retry with exponential backoff (delay gets longer each time) $http->retry( 3, 500 )->get( 'https://rate-limited-api.example.com/data' ); ``` ###### Exception Handling[​](#exception-handling "Direct link to Exception Handling") Control whether exceptions are thrown on errors using `throw_exception()` and `dont_throw_exception()` methods: ```php use Mantle\Http_Client\Factory; use Mantle\Http_Client\Http_Client_Exception; $http = new Factory(); // Throw exceptions on HTTP errors try { $http->throw_exception()->get( 'https://httpbin.org/status/500' ); } catch ( Http_Client_Exception $e ) { echo 'Request failed: ' . $e->getMessage(); echo 'Status code: ' . $e->getResponse()->status(); } // Default behavior - return Response object instead of throwing $response = $http->dont_throw_exception()->get( 'https://httpbin.org/status/404' ); if ( $response->failed() ) { echo 'Request failed with status: ' . $response->status(); } ``` ###### HTTP Request Methods[​](#http-request-methods "Direct link to HTTP Request Methods") Make requests using different HTTP verbs with `get()`, `head()`, `post()`, `put()`, `patch()`, and `delete()` methods: ```php use Mantle\Http_Client\Factory; $http = new Factory(); // GET request with query parameters $response = $http->get( 'https://httpbin.org/get', [ 'search' => 'mantle framework', 'limit' => 10, ] ); // GET request with query string in URL $response = $http->get( 'https://httpbin.org/get?foo=bar' ); // HEAD request (same parameters as GET) $response = $http->head( 'https://httpbin.org/get', [ 'check' => 'headers', ] ); // POST request with data $response = $http->post( 'https://httpbin.org/post', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'age' => 30, ] ); // PUT request for updates $response = $http->put( 'https://api.example.com/users/123', [ 'name' => 'Jane Doe', 'email' => 'jane@example.com', ] ); // PATCH request for partial updates $response = $http->patch( 'https://api.example.com/users/123', [ 'email' => 'newemail@example.com', ] ); // DELETE request $response = $http->delete( 'https://api.example.com/users/123' ); // DELETE request with data $response = $http->delete( 'https://api.example.com/posts/456', [ 'reason' => 'spam', 'notify_author' => true, ] ); ``` #### Responses[​](#responses "Direct link to Responses") The HTTP client will return an instance of `Mantle\Http_Client\Response`, which provides a flexible wrapper on top of the raw WordPress HTTP API response. ##### Available Response Methods[​](#available-response-methods "Direct link to Available Response Methods") The Response object provides a comprehensive set of methods for inspecting and working with HTTP responses: ```php $response->body(): mixed $response->client_error(): bool $response->collect( $key = null ): \Mantle\Support\Collection $response->cookie( string $name ): ?\WP_HTTP_Cookie $response->cookies(): array $response->dd() $response->dump() $response->failed(): bool $response->feed(): \SimplePie $response->forbidden(): bool $response->header( string $header ) $response->headers(): array $response->is_blob(): bool $response->is_feed(): bool $response->is_file(): bool $response->is_json(): bool $response->is_wp_error(): bool $response->is_xml(): bool $response->json( ?string $key = null, $default = null ) $response->mixed_json( ?string $key = null, $default = null ) $response->object() $response->ok(): bool $response->redirect(): bool $response->response(): array $response->server_error(): bool $response->status(): int $response->successful(): bool $response->unauthorized(): bool $response->xml( string $xpath = null, $default = null ) ``` * [Status Code Methods](#status-code-methods) * [Success Status Checks](#success-status-checks) * [Error Status Checks](#error-status-checks) * [Specific Status Checks](#specific-status-checks) * [Status Code Retrieval](#status-code-retrieval) * [Response Body Methods](#response-body-methods) * [Raw Body Access](#raw-body-access) * [JSON Parsing](#json-parsing) * [Mixed JSON Parsing](#mixed-json-parsing) * [XML Parsing](#xml-parsing) * [Collection Access](#collection-access) * [Feed Parsing](#feed-parsing) * [Header Methods](#header-methods) * [Cookie Methods](#cookie-methods) * [Content Type Detection Methods](#content-type-detection-methods) * [Text Content Detection](#text-content-detection) * [Binary Content Detection](#binary-content-detection) * [Feed Detection](#feed-detection) * [Error Detection](#error-detection) * [Raw Response Methods](#raw-response-methods) * [Debugging Methods](#debugging-methods) ###### Status Code Methods[​](#status-code-methods "Direct link to Status Code Methods") ###### Success Status Checks[​](#success-status-checks "Direct link to Success Status Checks") Check for successful HTTP responses using `ok()` and `successful()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for successful responses (2xx status codes) using ok() and successful() $response = Http::get( 'https://httpbin.org/status/200' ); $response->ok(); // true for 200 status $response->successful(); // true for 200-299 status codes ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for successful responses (2xx status codes) using ok() and successful() $response = $http->get( 'https://httpbin.org/status/200' ); $response->ok(); // true for 200 status $response->successful(); // true for 200-299 status codes ``` ###### Error Status Checks[​](#error-status-checks "Direct link to Error Status Checks") Check for HTTP errors using `failed()`, `client_error()`, and `server_error()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for error responses using failed(), client_error(), and server_error() $response = Http::get( 'https://httpbin.org/status/404' ); $response->failed(); // true for 400-599 status codes $response->client_error(); // true for 400-499 status codes $response->server_error(); // true for 500-599 status codes ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for error responses using failed(), client_error(), and server_error() $response = $http->get( 'https://httpbin.org/status/404' ); $response->failed(); // true for 400-599 status codes $response->client_error(); // true for 400-499 status codes $response->server_error(); // true for 500-599 status codes ``` ###### Specific Status Checks[​](#specific-status-checks "Direct link to Specific Status Checks") Check for specific HTTP status codes using `unauthorized()`, `forbidden()`, and `redirect()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for specific status codes using unauthorized(), forbidden(), and redirect() $response = Http::get( 'https://httpbin.org/status/401' ); $response->unauthorized(); // true for 401 status $response->forbidden(); // true for 403 status $response->redirect(); // true for 300-399 status codes ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for specific status codes using unauthorized(), forbidden(), and redirect() $response = $http->get( 'https://httpbin.org/status/401' ); $response->unauthorized(); // true for 401 status $response->forbidden(); // true for 403 status $response->redirect(); // true for 300-399 status codes ``` ###### Status Code Retrieval[​](#status-code-retrieval "Direct link to Status Code Retrieval") Get the exact HTTP status code using `status()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Get the exact status code using status() $response = Http::get( 'https://httpbin.org/status/200' ); $status = $response->status(); // Returns integer status code ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Get the exact status code using status() $response = $http->get( 'https://httpbin.org/status/200' ); $status = $response->status(); // Returns integer status code ``` ###### Response Body Methods[​](#response-body-methods "Direct link to Response Body Methods") ###### Raw Body Access[​](#raw-body-access "Direct link to Raw Body Access") Get the raw response body content using `body()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Get raw response body using body() $response = Http::get( 'https://httpbin.org/json' ); $body = $response->body(); // Returns string ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Get raw response body using body() $response = $http->get( 'https://httpbin.org/json' ); $body = $response->body(); // Returns string ``` ###### JSON Parsing[​](#json-parsing "Direct link to JSON Parsing") Parse JSON responses and access specific data using `json()` and `object()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Parse JSON responses using json() $response = Http::get( 'https://httpbin.org/json' ); $json = $response->json(); // Returns array $value = $response->json( 'slideshow.author' ); // Get specific JSON path $default = $response->json( 'nonexistent.key', 'default' ); // With default value // Parse as object using object() $object = $response->object(); // Returns stdClass object ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Parse JSON responses using json() $response = $http->get( 'https://httpbin.org/json' ); $json = $response->json(); // Returns array $value = $response->json( 'slideshow.author' ); // Get specific JSON path $default = $response->json( 'nonexistent.key', 'default' ); // With default value // Parse as object using object() $object = $response->object(); // Returns stdClass object ``` ###### Mixed JSON Parsing[​](#mixed-json-parsing "Direct link to Mixed JSON Parsing") Access specific JSON paths with type casting using `mixed_json()` which will return the value at the specified JSON path as a [`Mixed_Data`](/docs/features/support/mixed-data.md) instance: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Parse JSON responses using mixed_json() $response = Http::get( 'https://httpbin.org/json' ); $mixed = $response->mixed_json( 'slideshow.slides.0.title' ); // Returns Mixed_Data $title = $mixed->string(); // Cast to string ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Parse JSON responses using mixed_json() $response = $http->get( 'https://httpbin.org/json' ); $mixed = $response->mixed_json( 'slideshow.slides.0.title' ); // Returns Mixed_Data $title = $mixed->string(); // Cast to string ``` ###### XML Parsing[​](#xml-parsing "Direct link to XML Parsing") Parse XML responses with optional XPath queries using `xml()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Parse XML responses using xml() $response = Http::get( 'https://httpbin.org/xml' ); $xml = $response->xml(); // Returns array $nodes = $response->xml( '/slideshow/slide' ); // With XPath query ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Parse XML responses using xml() $response = $http->get( 'https://httpbin.org/xml' ); $xml = $response->xml(); // Returns array $nodes = $response->xml( '/slideshow/slide' ); // With XPath query ``` ###### Collection Access[​](#collection-access "Direct link to Collection Access") Access response data as a Mantle Collection using `collect()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Get as Collection using collect() $response = Http::get( 'https://httpbin.org/json' ); $collection = $response->collect(); // Returns Mantle\Support\Collection $specific = $response->collect( 'data' ); // Collect specific key ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Get as Collection using collect() $response = $http->get( 'https://httpbin.org/json' ); $collection = $response->collect(); // Returns Mantle\Support\Collection $specific = $response->collect( 'data' ); // Collect specific key ``` ###### Feed Parsing[​](#feed-parsing "Direct link to Feed Parsing") Parse RSS/Atom feeds and get a SimplePie object using `feed()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Parse RSS/Atom feeds using feed() $response = Http::get( 'https://example.org/feed/' ); $feed = $response->feed(); // Returns SimplePie object // Access feed data through SimplePie methods $title = $feed->get_title(); $description = $feed->get_description(); $items = $feed->get_items(); ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Parse RSS/Atom feeds using feed() $response = $http->get( 'https://example.org/feed/' ); $feed = $response->feed(); // Returns SimplePie object // Access feed data through SimplePie methods $title = $feed->get_title(); $description = $feed->get_description(); $items = $feed->get_items(); ``` ###### Header Methods[​](#header-methods "Direct link to Header Methods") Access response headers and metadata using `header()` and `headers()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::get( 'https://httpbin.org/headers' ); // Get specific header using header() $contentType = $response->header( 'Content-Type' ); $customHeader = $response->header( 'X-Custom-Header' ); // Get all headers using headers() $headers = $response->headers(); // Returns array of all headers ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); $response = $http->get( 'https://httpbin.org/headers' ); // Get specific header using header() $contentType = $response->header( 'Content-Type' ); $customHeader = $response->header( 'X-Custom-Header' ); // Get all headers using headers() $headers = $response->headers(); // Returns array of all headers ``` ###### Cookie Methods[​](#cookie-methods "Direct link to Cookie Methods") Work with response cookies using `cookie()` and `cookies()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::get( 'https://httpbin.org/cookies/set/session/abc123' ); // Get specific cookie using cookie() $cookie = $response->cookie( 'session' ); // Returns WP_HTTP_Cookie or null // Get all cookies using cookies() $cookies = $response->cookies(); // Returns array of WP_HTTP_Cookie objects ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); $response = $http->get( 'https://httpbin.org/cookies/set/session/abc123' ); // Get specific cookie using cookie() $cookie = $response->cookie( 'session' ); // Returns WP_HTTP_Cookie or null // Get all cookies using cookies() $cookies = $response->cookies(); // Returns array of WP_HTTP_Cookie objects ``` ###### Content Type Detection Methods[​](#content-type-detection-methods "Direct link to Content Type Detection Methods") ###### Text Content Detection[​](#text-content-detection "Direct link to Text Content Detection") Detect JSON and XML content types using `is_json()` and `is_xml()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check content types using is_json() and is_xml() $response = Http::get( 'https://httpbin.org/json' ); $response->is_json(); // true for JSON responses $response = Http::get( 'https://httpbin.org/xml' ); $response->is_xml(); // true for XML responses ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check content types using is_json() and is_xml() $response = $http->get( 'https://httpbin.org/json' ); $response->is_json(); // true for JSON responses $response = $http->get( 'https://httpbin.org/xml' ); $response->is_xml(); // true for XML responses ``` ###### Binary Content Detection[​](#binary-content-detection "Direct link to Binary Content Detection") Detect file and binary content using `is_file()` and `is_blob()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for file content using is_file() and is_blob() $response = Http::get( 'https://httpbin.org/image/png' ); $response->is_file(); // true for file downloads $response->is_blob(); // true for binary/file content ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for file content using is_file() and is_blob() $response = $http->get( 'https://httpbin.org/image/png' ); $response->is_file(); // true for file downloads $response->is_blob(); // true for binary/file content ``` ###### Feed Detection[​](#feed-detection "Direct link to Feed Detection") Detect and parse RSS/Atom feeds using `is_feed()` and `feed()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for RSS/Atom feeds using is_feed() and feed() $response = Http::get( 'https://example.org/feed/' ); $response->is_feed(); // true for RSS/Atom feeds $feed = $response->feed(); // Returns SimplePie object ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for RSS/Atom feeds using is_feed() and feed() $response = $http->get( 'https://example.org/feed/' ); $response->is_feed(); // true for RSS/Atom feeds $feed = $response->feed(); // Returns SimplePie object ``` ###### Error Detection[​](#error-detection "Direct link to Error Detection") Check for WordPress errors using `is_wp_error()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Check for WordPress errors using is_wp_error() $response = Http::get( 'https://httpbin.org/status/404' ); $response->is_wp_error(); // true if response is a WP_Error ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Check for WordPress errors using is_wp_error() $response = $http->get( 'https://httpbin.org/status/404' ); $response->is_wp_error(); // true if response is a WP_Error ``` ###### Raw Response Methods[​](#raw-response-methods "Direct link to Raw Response Methods") Access underlying WordPress HTTP API response using `response()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::get( 'https://httpbin.org/get' ); // Get the raw WordPress HTTP response array using response() $raw = $response->response(); // Returns array from wp_remote_get() ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); $response = $http->get( 'https://httpbin.org/get' ); // Get the raw WordPress HTTP response array using response() $raw = $response->response(); // Returns array from wp_remote_get() ``` ###### Debugging Methods[​](#debugging-methods "Direct link to Debugging Methods") Debug and inspect responses during development using `dump()` and `dd()`: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::get( 'https://httpbin.org/json' ); // Dump response for debugging using dump() $response->dump(); // Outputs response details and continues // Dump and die (stops execution) using dd() $response->dd(); // Outputs response details and stops ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); $response = $http->get( 'https://httpbin.org/json' ); // Dump response for debugging using dump() $response->dump(); // Outputs response details and continues // Dump and die (stops execution) using dd() $response->dd(); // Outputs response details and stops ``` ##### Array Access Support[​](#array-access-support "Direct link to Array Access Support") The `Mantle\Http_Client\Response` object implements `ArrayAccess`, allowing you to access JSON/XML response properties directly: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; // Access JSON properties like an array $response = Http::get( 'https://httpbin.org/json' ); $slideshow = $response['slideshow']; // Equivalent to $response->json('slideshow') $author = $response['slideshow']['author']; // Nested access ``` ```php use Mantle\Http_Client\Factory; $http = new Factory(); // Access JSON properties like an array $response = $http->get( 'https://httpbin.org/json' ); $slideshow = $response['slideshow']; // Equivalent to $response->json('slideshow') $author = $response['slideshow']['author']; // Nested access ``` #### Base API Client[​](#base-api-client "Direct link to Base API Client") The HTTP Client can be used as a basis for a API Client for an external service. For example, if you are making requests to `https://httpbin.org` and want to include built-in authentication, you could extend `Mantle\Http_Client\Factory` and implement a `new_pending_request` method: ```php use Mantle\Http_Client\Factory; use Mantle\Http_Client\Pending_Request; class HttpBinClient extends Factory { protected function new_pending_request(): Pending_Request { return ( new Pending_Request() ) ->base_url( 'https://httpbin.org' ) ->with_token( '' ); } ``` This is a great pattern to use when creating clients for different APIs. Because it extends `Factory`, your IDE will also pick up all the methods available to the client. Let's see an example of using the `HttpBinClient` class: ```php // Make a POST request to https://httpbin.org/example-endpoint $request = HttpBinClient::create()->post( '/example-endpoint', [ 'name' => 'Mantle Http Client', ] ); ``` #### Middleware[​](#middleware "Direct link to Middleware") Requests can have middleware applied to them to allow the request and the response to be modified. One use case is to calculate a checksum header based on body of the request and pass that along with the request: * Framework Use * Standalone Use ```php use Closure; use Mantle\Facade\Http; $client = Http::base_url( 'https://api.github.com' ) ->middleware( function ( Http_Client $client, Closure $next ) { $client->with_header( 'Authorization', md5( $client->url() . $client->body() ) ); // You can also run the callback and then modify the response. return $next( $client ); } ); ``` ```php use Closure; use Mantle\Http_Client\Factory; $client = Factory::base_url( 'https://api.github.com' ) ->middleware( function ( Http_Client $client, Closure $next ) { $client->with_header( 'Authorization', md5( $client->url() . $client->body() ) ); // You can also run the callback and then modify the response. return $next( $client ); } ); ``` #### Caching[​](#caching "Direct link to Caching") Requests can be cached using the `cached` method. The method can be used when chaining together your HTTP request: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::cached()->get( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; $response = Factory::create()->cached()->get( 'https://example.org' ); ``` The `cached` method accepts a single argument of the number of seconds to cache the response for. It also supports a `DateTime` object and defaults to one hour: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::cached( now()->addDay() )->get( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; $response = Factory::create() ->cached( now()->addDay() ) ->get( 'https://example.org' ); ``` Purging the cache can be done using the `purge` method: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; Http::url( 'https://example.org' )->purge(); ``` ```php use Mantle\Http_Client\Factory; Factory::create()->url( 'https://example.org' )->purge(); ``` ##### Flexible Caching (SWR)[​](#flexible-caching-swr "Direct link to Flexible Caching (SWR)") By default, when a cached response expires the HTTP Client will make a new request and return the new response. If you would like to return the stale cached response while a new request is made in the background, you can use flexible caching (a pattern known as SWR - Stale While Revalidate). Stale While Revalidate will reuse the stale cached response while a new request is made in the background. This can be useful when you want to ensure that your application is always responsive, even if the external service is slow or unavailable. The new request will be made after the page has been sent to the browser, so it won't impact the user experience. To use SWR caching, use the `cache_flexible()` method: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; use function Mantle\Support\Helpers\now; $response = Http::cache_flexible( stale: now()->addMinutes( 30 ), expire: now()->addHours( 3 ), )->get( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; use function Mantle\Support\Helpers\now; $response = Factory::create() ->cache_flexible( stale: now()->addMinutes( 30 ), expire: now()->addHours( 3 ), ) ->get( 'https://example.org' ); ``` The `cache_flexible` method accepts two arguments: * `stale`: The amount of time to keep the cached response stale. Accepts a number of seconds or a `DateTime` instance. Also accepts a `Closure` that is passed a request and response instance to dynamically determine the stale time. * `expire`: The amount of time to keep the cached response before it is considered expired. Accepts a number of seconds or a `DateTime` instance. Also accepts a `Closure` that is passed a request and response instance to dynamically determine the expire time. Here is an example of passing a number of seconds to the `cache_flexible` method: * Framework Use * Standalone Use ```php use Mantle\Facade\Http; $response = Http::cache_flexible( stale: HOUR_IN_SECONDS, // 1 hour expire: DAY_IN_SECONDS, // 24 hours )->get( 'https://example.org' ); ``` ```php use Mantle\Http_Client\Factory; $response = Factory::create() ->cache_flexible( stale: HOUR_IN_SECONDS, // 1 hour expire: DAY_IN_SECONDS, // 24 hours ) ->get( 'https://example.org' ); ``` #### Concurrent Requests (HTTP Pooling)[​](#concurrent-requests-http-pooling "Direct link to Concurrent Requests (HTTP Pooling)") Sometimes, you may wish to make multiple HTTP requests concurrently. In other words, you want several requests to be dispatched at the same time instead of issuing the requests sequentially. This can lead to substantial performance improvements when interacting with slow HTTP APIs. Thankfully, you may accomplish this using the `pool` method. The `pool` method accepts a closure which receives an `Mantle\Http_Client\Pool` instance, allowing you to easily add requests to the request pool for dispatching: * Framework Use * Standalone Use ```php use Mantle\Http_Client\Pool; use Mantle\Facade\Http; $responses = Http::pool( fn ( Pool $pool ) => [ $pool->get( 'http://localhost/first' ), $pool->post( 'http://localhost/second' )->with_json( [ 'name' => 'Mantle' ] ), $pool->get( 'http://localhost/third' ), ] ); return $responses[0]->ok() && $responses[1]->ok() && $responses[2]->ok(); ``` ```php use Mantle\Http_Client\Pool; use Mantle\Http_Client\Factory; $responses = Factory::create()->pool( fn ( Pool $pool ) => [ $pool->get( 'http://localhost/first' ), $pool->post( 'http://localhost/second' )->with_json( [ 'name' => 'Mantle' ] ), $pool->get( 'http://localhost/third' ), ] ); return $responses[0]->ok() && $responses[1]->ok() && $responses[2]->ok(); ``` As you can see, each response instance can be accessed based on the order it was added to the pool. If you wish, you can name the requests using the `as` method, which allows you to access the corresponding responses by name: * Framework Use * Standalone Use ```php use Mantle\Http_Client\Pool; use Mantle\Facade\Http; $responses = Http::pool( fn ( Pool $pool ) => [ $pool->as( 'first' )->get( 'http://localhost/first' ), $pool->as( 'second' )->get( 'http://localhost/second' ), $pool->as( 'third' )->get( 'http://localhost/third' ), ] ); return $responses['first']->ok() && $responses['second']->ok() && $responses['third']->ok(); ``` ```php use Mantle\Http_Client\Pool; use Mantle\Http_Client\Factory; $responses = Factory::create()->pool( fn ( Pool $pool ) => [ $pool->as( 'first' )->get( 'http://localhost/first' ), $pool->as( 'second' )->get( 'http://localhost/second' ), $pool->as( 'third' )->get( 'http://localhost/third' ), ] ); return $responses['first']->ok() && $responses['second']->ok() && $responses['third']->ok(); ``` Pooled requests can use any of the methods available on the HTTP Client, such as `with_headers()`, `with_token()`, `timeout()`, as well as `cache()` and `cache_flexible()` for caching responses. #### Testing Http Client[​](#testing-http-client "Direct link to Testing Http Client") The Mantle Http Client was built using WordPress' built-in HTTP API. This means that it can be tested using [Mantle's Remote Request](/docs/testing/remote-requests.md) testing functionality. --- ### Queues Mantle provides a Queue interface for queueing asynchronous jobs that should be run in the background instead of blocking the user's request. The process is abstracted so that another queue provider could be used but only WordPress-backed queues are supported out of the box. Mantle's Queue System Can Be Used In Any WordPress Project Mantle's queue system is designed to be used in any WordPress project, not just those built with Mantle. You can instantiate the bootloader and then use the queue without any additional setup. ```php use function Mantle\Queue\dispatch; // Instantiate the bootloader. bootloader()->boot(); // Dispatch a job. dispatch( function () { // Perform some task... } ); ``` #### Creating Jobs[​](#creating-jobs "Direct link to Creating Jobs") Application jobs are stored in the `apps/jobs` directory. Jobs can be generated through the `wp-cli` command: ```bash bin/mantle make:job Send_Welcome_Email ``` That will generate a new job class in `app/jobs/class-send-welcome-email.php` that looks like this: ```php namespace App\Jobs; use Mantle\Contracts\Queue\Can_Queue; use Mantle\Contracts\Queue\Job; use Mantle\Queue\Dispatchable; use Mantle\Queue\Queueable; /** * Send_Welcome_Email Job. */ class Send_Welcome_Email implements Job, Can_Queue { use Queueable, Dispatchable; /** * Handle the job. */ public function handle() { // Handle it here! } } ``` #### Dispatching Jobs[​](#dispatching-jobs "Direct link to Dispatching Jobs") Once you have a job class you can dispatch to it from anywhere in your application. ```php use App\Jobs\Send_Welcome_Email; Send_Welcome_Email::dispatch( $arguments_to_pass_to_constructor ); ``` Any arguments passed to the `dispatch` method will be passed to the job's constructor. The job will be serialized and stored in the database until it is processed. You can also dispatch using the `dispatch` helper function: ```php dispatch( new Send_Welcome_Email( $arguments_to_pass_to_constructor ) ); ``` If you need any application services in your job, you can type-hint them in the handle method and they will be injected automatically: ```php namespace App\Jobs; use App\Models\Post; use App\Services\Example_Service; use Mantle\Contracts\Queue\Can_Queue; use Mantle\Contracts\Queue\Job; use Mantle\Queue\Dispatchable; use Mantle\Queue\Queueable; class Send_Welcome_Email implements Job, Can_Queue { use Queueable, Dispatchable; /** * Create a new job instance. * * @param Post $post */ public function __construct( public Post $post ) {} /** * Handle the job. */ public function handle( Example_Service $service ) { // Service is automatically injected at runtime. } } ``` The above job can be dispatched like this: ```php $post = App\Models\Post::find( 1 ); Example_Job::dispatch( $post ); ``` ##### Dispatching Closures/Anonymous Functions[​](#dispatching-closuresanonymous-functions "Direct link to Dispatching Closures/Anonymous Functions") Closures can be dispatched to the queue as well. The closure will be serialized and unserialized when it is run. Dispatching an anonymous function is an incredibly powerful tool that can quickly take an expensive operation and move it to the background. ```php $post = App\Models\Post::find( 1 ); dispatch( function() use ( $post ) { $post->perform_expensive_operation(); } ); ``` Using the `catch` method, you may provide a closure that should be executed if the queued closure fails to complete successfully after exhausting all of your queue's configured retry attempts. ```php dispatch( function() { // Perform some task... } )->catch( function( Exception $e ) { // Send the exception to Sentry, etc... } ); ``` ##### Dispatch Synchronously[​](#dispatch-synchronously "Direct link to Dispatch Synchronously") A job can be invoked synchronously and will be run in the current request. ```php Example_Job::dispatch_now(); ``` ##### Dispatching After the Request is Sent[​](#dispatching-after-the-request-is-sent "Direct link to Dispatching After the Request is Sent") If you need to dispatch a job after the response has been sent to the user, you can use the `dispatch_after_response` method. This is useful for deferring actions that should be executed after the response has been sent to the user but before the request is completed. ```php dispatch( function () { // Perform some task... } )->after_response(); // Or with a job class: Example_Job::dispatch_after_response(); ``` ##### Multiple Queues[​](#multiple-queues "Direct link to Multiple Queues") To allow for some priority between jobs a job can be sent to a specific queue. By default all jobs will be sent to the `default` queue. A job can be selectively sent to a smaller/different queue to allow for some priority among jobs. ```php // Sent to the 'default' queue. Example_Job::dispatch(); // Sent to the 'priority' queue. Example_Job::dispatch()->on_queue( 'priority' ); ``` Dispatching to an isolated/smaller queue can be useful for ensuring that a specific job is processed before others. For example, a job that sends a welcome email to a new user might be sent to a `welcome-email` queue to ensure that it is processed before other jobs that are sent to a larger `default` queue. #### Queue Worker[​](#queue-worker "Direct link to Queue Worker") The queue worker is a scheduled task that runs via cron to process batches of queued jobs. When a queue job is dispatched, it is stored in the database and a queue worker batch is scheduled to run. The queue worker will process a batch of 5 jobs by default and can be configured via the `queue.batch_size` configuration value or `QUEUE_BATCH_SIZE` environment variable. The queue worker also supports concurrent processing of job batches. By default, only a single batch will be processed at a time. For sites with a lot of queued jobs, it may be beneficial to process multiple batches concurrently. This can be configured via the `queue.max_concurrent_batches` configuration value or `QUEUE_MAX_CONCURRENT_BATCHES` environment variable. The default value is 1, meaning only a single batch will be processed at a time. A value of 2 would allow for two batches to be processed concurrently, and so on. The value should be adjusted based on the site's server resources and the number of queued jobs. Jobs will be deleted from the queue after a week once they have either completed or failed. This can be configured via the `queue.delete_after` configuration value or `QUEUE_DELETE_AFTER` environment variable. #### Queue Admin[​](#queue-admin "Direct link to Queue Admin") The WordPress-powered queue includes an admin page for monitoring the queue status, reviewing pending/failed/completed jobs, and retrying failed jobs. It is accessible via the WordPress admin under the `Tools` menu: ![Queue Admin](/assets/images/queue-08927c9b7c1041d4f1713ee0f575cc34.png) --- ### Scheduling Tasks #### Introduction[​](#introduction "Direct link to Introduction") To help provide a singular interface for scheduling jobs in WordPress (and to not interact with WordPress cron) Mantle provides a fluent interface for defining schedule-able tasks. Underneath Mantle uses a every minute WP cron job to trigger the scheduled tasks. This allows you to define your scheduled tasks in a more expressive way. #### Defining Schedules[​](#defining-schedules "Direct link to Defining Schedules") Jobs, console commands, or closures may be scheduled in your `routes/console.php` file. routes/console.php ```php use Mantle\Facade\Schedule; // Schedule a job class. Schedule::job( \App\Jobs\Example_Job::class )->daily(); // Schedule a console command. Schedule::command( \App\Console\Example_Command::class )->hourly(); Schedule::call( function() { // Do something great! } )->weekly(); ``` ##### Schedule Frequency Options[​](#schedule-frequency-options "Direct link to Schedule Frequency Options") There are a variety of schedules you may assign to your task: | Method | Description | | ----------------------------------- | ------------------------------------------------------- | | `->cron( '* * * * *' );` | Run the task on a custom Cron schedule | | `->everyMinute();` | Run the task every minute | | `->everyTwoMinutes();` | Run the task every two minutes | | `->everyThreeMinutes();` | Run the task every three minutes | | `->everyFourMinutes();` | Run the task every four minutes | | `->everyFiveMinutes();` | Run the task every five minutes | | `->everyTenMinutes();` | Run the task every ten minutes | | `->everyFifteenMinutes();` | Run the task every fifteen minutes | | `->everyThirtyMinutes();` | Run the task every thirty minutes | | `->hourly();` | Run the task every hour | | `->hourlyAt(17);` | Run the task every hour at 17 minutes past the hour | | `->everyTwoHours();` | Run the task every two hours | | `->everyThreeHours();` | Run the task every three hours | | `->everyFourHours();` | Run the task every four hours | | `->everySixHours();` | Run the task every six hours | | `->daily();` | Run the task every day at midnight | | `->dailyAt( '13:00' );` | Run the task every day at 13:00 | | `->twiceDaily(1, 13);` | Run the task daily at 1:00 & 13:00 | | `->weekly();` | Run the task every sunday at 00:00 | | `->weeklyOn(1, '8:00' );` | Run the task every week on Monday at 8:00 | | `->monthly();` | Run the task on the first day of every month at 00:00 | | `->monthlyOn(4, '15:00' );` | Run the task every month on the 4th at 15:00 | | `->monthlyOnLastDay( '15:00' );` | Run the task on the last day of the month at 15:00 | | `->quarterly();` | Run the task on the first day of every quarter at 00:00 | | `->yearly();` | Run the task on the first day of every year at 00:00 | | `->timezone( 'America/New_York' );` | Set the timezone | These methods may be combined with additional constraints to create even more finely tuned schedules that only run on certain days of the week. For example, to schedule a command to run weekly on Monday: ```php // Run once per week on Monday at 1 PM... $schedule->call( function () { // } )->weekly()->mondays()->at( '13:00' ); // Run hourly from 8 AM to 5 PM on weekdays... $schedule->command( Example_Command::class ) ->weekdays() ->hourly() ->timezone( 'America/Chicago' ) ->between( '8:00', '17:00' ); ``` Below is a list of the additional schedule constraints: | Method | Description | | ---------------------------- | ----------------------------------------------------------------------------------------------- | | `->weekdays();` | Limit the task to weekdays | | `->weekends();` | Limit the task to weekends | | `->sundays();` | Limit the task to Sunday | | `->mondays();` | Limit the task to Monday | | `->tuesdays();` | Limit the task to Tuesday | | `->wednesdays();` | Limit the task to Wednesday | | `->thursdays();` | Limit the task to Thursday | | `->fridays();` | Limit the task to Friday | | `->saturdays();` | Limit the task to Saturday | | \`->days( array | mixed );\` | | `->between( $start, $end );` | Limit the task to run between start and end times | | `->when( Closure );` | Limit the task based on a truth test | | `->environments($env);` | Limit the task to specific environments, accepts multiple environments as additional arguments. | ###### Day Constraints[​](#day-constraints "Direct link to Day Constraints") The `days` method may be used to limit the execution of a task to specific days of the week. For example, you may schedule a command to run hourly on Sundays and Wednesdays: ```php $schedule->command( Example_Command::class ) ->hourly() ->days([0, 3]); ``` ###### Between Time Constraints[​](#between-time-constraints "Direct link to Between Time Constraints") The `between` method may be used to limit the execution of a task based on the time of day: ```php $schedule->command( Example_Command::class ) ->hourly() ->between( '7:00', '22:00' ); ``` Similarly, the `unlessBetween` method can be used to exclude the execution of a task for a period of time: ```php $schedule->command( Example_Command::class ) ->hourly() ->unlessBetween( '23:00', '4:00' ); ``` ###### Truth Test Constraints[​](#truth-test-constraints "Direct link to Truth Test Constraints") The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other words, if the given `Closure` returns `true`, the task will execute as long as no other constraining conditions prevent the task from running: ```php $schedule->command( Example_Command::class ) ->daily() ->when( function () { return true; } ); ``` The `skip` method may be seen as the inverse of `when`. If the `skip` method returns `true`, the scheduled task will not be executed: ```php $schedule->command( Example_Command::class ) ->daily() ->skip( function () { return true; } ); ``` When using chained `when` methods, the scheduled command will only execute if all `when` conditions return `true`. ###### Environment Constraints[​](#environment-constraints "Direct link to Environment Constraints") The `environments` method may be used to execute tasks only on the given environments: ```php $schedule->command( Example_Command::class ) ->daily() ->environments( 'staging', 'production' ); ``` #### Future Plans[​](#future-plans "Direct link to Future Plans") In the future we plan on adding in additional protection for task overlapping and task concurrency. --- ### Support The `mantle-framework/support` package provides a set of classes and traits that can be used to extend the functionality of your classes/application. This package doesn't require the rest of the framework in anyway and is a great addition to any WordPress project. #### Installation[​](#installation "Direct link to Installation") You can install the package via Composer: ```bash composer require mantle-framework/support ``` #### Features[​](#features "Direct link to Features") The following are features included in the `mantle-framework/support` package: #### [📄️ Classname](/docs/features/support/classname.md) [A classname helper function for generating CSS class names based on various conditions and inputs.](/docs/features/support/classname.md) #### [📄️ Collections](/docs/features/support/collections.md) [{/ Collection methods should be added in alphabetical order /}](/docs/features/support/collections.md) #### [📄️ Conditionable](/docs/features/support/conditionable.md) [Conditionable is a trait that provides a way to conditionally call a closure](/docs/features/support/conditionable.md) #### [📄️ Helpers](/docs/features/support/helpers.md) [General helpers for development.](/docs/features/support/helpers.md) #### [📄️ Hookable](/docs/features/support/hookable.md) [Hookable is a trait that will automatically register methods on your class with](/docs/features/support/hookable.md) #### [📄️ HTML Manipulation](/docs/features/support/html.md) [The HTML class provides methods to query, manipulate, and assert against HTML](/docs/features/support/html.md) #### [📄️ Macroable](/docs/features/support/macroable.md) [Macros allow you to add methods to classes from outside the class. This is](/docs/features/support/macroable.md) #### [📄️ Mixed Options/Meta Data](/docs/features/support/mixed-data.md) [Handle mixed data types in a type-safe manner for options and object metadata.](/docs/features/support/mixed-data.md) #### [📄️ Pipeline](/docs/features/support/pipeline.md) [Execute a series of tasks in a specific order using the pipeline pattern.](/docs/features/support/pipeline.md) #### [📄️ Singleton](/docs/features/support/singleton.md) [A singleton is a class that can only be instantiated once. It is often used to](/docs/features/support/singleton.md) #### [📄️ Stringable](/docs/features/support/stringable.md) [The Stringable class exists in the Mantle\Support namespace and is used to create a stringable object from a string or a stringable object.](/docs/features/support/stringable.md) #### [📄️ URI Manipulation](/docs/features/support/uri.md) [Manipulate and work with URIs using Mantle's fluent URI class and helpers](/docs/features/support/uri.md) --- ### Classname Mantle provides a `classname` helper function designed to simplify the generation of CSS class names. This function operates similarly to the popular NPM `classnames` package, allowing you to dynamically construct class names based on various conditions and inputs. #### Usage[​](#usage "Direct link to Usage") The `classname` function can be used to generate a class name based on the provided arguments. The function accepts any number of arguments and will concatenate them together to form a single class name. The function will ignore any arguments that are `null`, `false`, or an empty string. ```php use function Mantle\Support\Helpers\classname; classname( 'class1', 'class2', 'class3' ); // 'class1 class2 class3' classname( 'class1', null, 'class3' ); // 'class1 class3' classname( 'class1', false, 'class3' ); // 'class1 class3' classname( 'foo', [ 'a-bunch', 'of-class-names', 'that-are-valid', ], // Conditionally add a class name. [ 'false' => false, 'valid' => true, ], ); // 'foo a-bunch of-class-names that-are-valid valid' classname( 'example', [ 'callable-based' => fn () => true, ], ); // 'example callable-based' ``` The `the_classnames` method will echo the class names directly to the output buffer. ```php use function Mantle\Support\Helpers\the_classnames; ?>
Mantle includes [Laravel's collections package](https://laravel.com/docs/master/collections) with support for all of its methods and with some special customizations to make it easier to work with WordPress. For example, you can pass a `WP_Query` object to the `collect` helper function and it will automatically convert it to a collection of `WP_Post` objects. #### Usage[​](#usage "Direct link to Usage") You can use the `collect` helper function to create a new collection from an array or an object. You can also use the `collect` method on any collection instance to create a new collection from the items in the original collection. ```php use function Mantle\Support\Helpers\collect; $collection = collect( [ 1, 2, 3 ] ); ``` You can then use any of the methods available on the collection instance to manipulate the items in the collection. ```php use function Mantle\Support\Helpers\collect; $collection = collect( [ 1, 2, 3 ] )->map( function( $item ) { return $item * 2; } ); ``` Collections can be treated like arrays, so you can use array accessors to get items from the collection. ```php use function Mantle\Support\Helpers\collect; $collection = collect( [ 1, 2, 3 ] ); $item = $collection[0]; // 1 ``` #### Available Methods[​](#available-methods "Direct link to Available Methods") * [add](#add) * [after](#after) * [all](#all) * [avg](#avg) * [average](#average) * [before](#before) * [chunk](#chunk) * [chunk\_while](#chunk_while) * [collapse](#collapse) * [collect](#collect) * [combine](#combine) * [concat](#concat) * [contains](#contains) * [contains\_one\_item](#contains_one_item) * [contains\_strict](#contains_strict) * [count](#count) * [count\_by](#count_by) * [cross\_join](#cross_join) * [dd](#dd) * [diff](#diff) * [diff\_assoc](#diff_assoc) * [diff\_assoc\_using](#diff_assoc_using) * [diff\_keys](#diff_keys) * [doesnt\_contain](#doesnt_contain) * [dump](#dump) * [duplicates](#duplicates) * [duplicates\_strict](#duplicates_strict) * [each](#each) * [each\_spread](#each_spread) * [empty](#empty) * [every](#every) * [except](#except) * [filter](#filter) * [first](#first) * [first\_or\_fail](#first_or_fail) * [first\_where](#first_where) * [flat\_map](#flat_map) * [flatten](#flatten) * [flip](#flip) * [for\_page](#for_page) * [forget](#forget) * [get](#get) * [get\_caching\_iterator](#get_caching_iterator) * [group\_by](#group_by) * [has](#has) * [implode](#implode) * [implode\_str](#implode_str) * [intersect](#intersect) * [intersect\_assoc](#intersect_assoc) * [intersect\_by\_keys](#intersect_by_keys) * [is\_empty](#is_empty) * [is\_not\_empty](#is_not_empty) * [join](#join) * [key\_by](#key_by) * [keys](#keys) * [last](#last) * [make](#make) * [map](#map) * [map\_into](#map_into) * [map\_spread](#map_spread) * [map\_to\_dictionary](#map_to_dictionary) * [map\_to\_groups](#map_to_groups) * [map\_with\_keys](#map_with_keys) * [max](#max) * [median](#median) * [merge](#merge) * [merge\_recursive](#merge_recursive) * [min](#min) * [mode](#mode) * [nth](#nth) * [only](#only) * [only\_children](#only_children) * [pad](#pad) * [partition](#partition) * [pipe](#pipe) * [pipe\_into](#pipe_into) * [pipe\_through](#pipe_through) * [pluck](#pluck) * [pop](#pop) * [prepend](#prepend) * [prepend\_many](#prepend_many) * [pull](#pull) * [push](#push) * [put](#put) * [random](#random) * [reduce](#reduce) * [reduce\_spread](#reduce_spread) * [reject](#reject) * [replace](#replace) * [replace\_recursive](#replace_recursive) * [reverse](#reverse) * [search](#search) * [shift](#shift) * [shuffle](#shuffle) * [skip](#skip) * [skip\_until](#skip_until) * [skip\_while](#skip_while) * [slice](#slice) * [sliding](#sliding) * [sole](#sole) * [some](#some) * [sort](#sort) * [sort\_by](#sort_by) * [sort\_by\_desc](#sort_by_desc) * [sort\_desc](#sort_desc) * [sort\_keys](#sort_keys) * [sort\_keys\_desc](#sort_keys_desc) * [sort\_keys\_using](#sort_keys_using) * [splice](#splice) * [split](#split) * [split\_in](#split_in) * [sum](#sum) * [take](#take) * [take\_until](#take_until) * [take\_while](#take_while) * [tap](#tap) * [times](#times) * [to\_array](#to_array) * [to\_base](#to_base) * [to\_json](#to_json) * [to\_pretty\_json](#to_pretty_json) * [toArray](#toarray) * [transform](#transform) * [trim](#trim) * [undot](#undot) * [unique](#unique) * [unique\_strict](#unique_strict) * [unless\_empty](#unless_empty) * [unless\_not\_empty](#unless_not_empty) * [unwrap](#unwrap) * [values](#values) * [when\_empty](#when_empty) * [when\_not\_empty](#when_not_empty) * [where](#where) * [where\_between](#where_between) * [where\_in](#where_in) * [where\_in\_strict](#where_in_strict) * [where\_instance\_of](#where_instance_of) * [where\_not\_between](#where_not_between) * [where\_not\_in](#where_not_in) * [where\_not\_in\_strict](#where_not_in_strict) * [where\_not\_null](#where_not_null) * [where\_null](#where_null) * [where\_strict](#where_strict) * [wrap](#wrap) * [zip](#zip) ##### add[​](#add "Direct link to add") The `add` method adds an item to the collection. ```php $collection = collect( [ 1, 2, 3 ] )->add( 4 ); ``` ##### after[​](#after "Direct link to after") Get the item after the given item in the collection. You can pass a value or a callback to determine the target item. Returns null if the item is the last in the collection or not found. ```php $collection = collect( [ 'a', 'b', 'c', 'd' ] ); $after = $collection->after( 'b' ); // 'c' $after = $collection->after( function( $item ) { return $item === 'c'; } ); // 'd' ``` ##### all[​](#all "Direct link to all") The `all` method gets all of the items in the collection. ```php $items = collect( [ 1, 2, 3 ] )->all(); ``` ##### avg[​](#avg "Direct link to avg") Get the average value of a given key. ```php $average = collect( [ 1, 2, 3 ] )->average(); $average = collect( [ [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->average( 'age' ); ``` ##### average[​](#average "Direct link to average") Alias for the `avg` method. ##### before[​](#before "Direct link to before") Get the item before the given item in the collection. You can pass a value or a callback to determine the target item. Returns null if the item is the first in the collection or not found. ```php $collection = collect( [ 'a', 'b', 'c', 'd' ] ); $before = $collection->before( 'c' ); // 'b' $before = $collection->before( function( $item ) { return $item === 'b'; } ); // 'a' ``` ##### chunk[​](#chunk "Direct link to chunk") Chunk the collection into smaller collections of a given size. ```php $chunks = collect( [ 1, 2, 3, 4, 5 ] )->chunk( 2 ); // [[1, 2], [3, 4], [5]] ``` ##### chunk\_while[​](#chunk_while "Direct link to chunk_while") Chunk the collection into groups as long as the given callback returns true. Each group will contain consecutive items until the callback returns false. ```php $chunks = collect( [1, 2, 3, 4, 5] )->chunk_while( function( $value, $key, $chunk ) { return $value - $chunk->last() <= 1; } ); // [[1, 2, 3], [4, 5]] ``` ##### collapse[​](#collapse "Direct link to collapse") Collapse the collection of arrays into a single, flat collection. ```php $collection = collect( [ [ 1, 2, 3 ], [ 4, 5, 6 ] ] )->collapse(); // [1, 2, 3, 4, 5, 6] ``` ##### collect[​](#collect "Direct link to collect") Convert the collection to a base Mantle collection instance. Useful if you are working with a subclass and want a plain collection. ```php $base = $collection->collect(); ``` ##### combine[​](#combine "Direct link to combine") Combine the values of the collection with the values of another array or collection. ```php $collection = collect( [ 'name', 'age' ] )->combine( [ 'John', 30 ] ); // ['name' => 'John', 'age' => 30] ``` ##### concat[​](#concat "Direct link to concat") Concatenate values of the given array or collection to the collection. ```php $collection = collect( [ 1, 2, 3 ] )->concat( [ 4, 5, 6 ] ); // [1, 2, 3, 4, 5, 6] ``` ##### contains[​](#contains "Direct link to contains") Determine if the collection contains a given item. ```php $contains = collect( [ 1, 2, 3 ] )->contains( 2 ); // true ``` ##### contains\_one\_item[​](#contains_one_item "Direct link to contains_one_item") Determine if the collection contains only one item. ```php $containsOneItem = collect( [ 1 ] )->contains_one_item(); // true ``` ##### contains\_strict[​](#contains_strict "Direct link to contains_strict") Determine if the collection contains a given key / value pair. ```php $contains = collect( [ 'name' => 'John' ] )->contains_strict( 'name', 'John' ); // true ``` ##### count[​](#count "Direct link to count") Get the number of items in the collection. ```php $count = collect( [ 1, 2, 3 ] )->count(); // 3 ``` ##### count\_by[​](#count_by "Direct link to count_by") Count the occurrences of values in the collection. ```php $counts = collect( [ 1, 2, 2, 3, 3, 3 ] )->count_by(); // [1 => 1, 2 => 2, 3 => 3] ``` ##### cross\_join[​](#cross_join "Direct link to cross_join") Cross join with the given lists, returning all possible permutations. ```php $collection = collect( [ 1, 2 ] )->cross_join( [ 'a', 'b' ] ); ``` ##### dd[​](#dd "Direct link to dd") Dump the collection and end the script execution. ```php collect( [ 1, 2, 3 ] )->dd(); ``` ##### diff[​](#diff "Direct link to diff") Compare the collection against another item or items using strict comparison. ```php $diff = collect( [ 1, 2, 3 ] )->diff( [ 2, 3, 4 ] ); // [0 => 1] ``` ##### diff\_assoc[​](#diff_assoc "Direct link to diff_assoc") Diff the collection with the given items, key and value-wise. ```php $diff = collect( [ 'name' => 'John' ] )->diff_assoc( [ 'name' => 'Jane' ] ); // ['name' => 'John'] ``` ##### diff\_assoc\_using[​](#diff_assoc_using "Direct link to diff_assoc_using") Diff the collection with the given items, using a callback to compute the difference. ```php $diff = collect( [ 'name' => 'John' ] )->diff_assoc_using( [ 'name' => 'Jane' ], function( $a, $b ) { return $a === $b; } ); // ['name' => 'John'] ``` ##### diff\_keys[​](#diff_keys "Direct link to diff_keys") Diff the collection with the given items, key-wise. ```php $diff = collect( [ 'name' => 'John' ] )->diff_keys( [ 'name' => 'Jane' ] ); ``` ##### doesnt\_contain[​](#doesnt_contain "Direct link to doesnt_contain") Determine if the collection does not contain a given key / value pair. ```php $doesntContain = collect( [ 'name' => 'John' ] )->doesnt_contain( 'name', 'Jane' ); // true ``` ##### dump[​](#dump "Direct link to dump") Dump the collection. ```php collect( [ 1, 2, 3 ] )->dump(); ``` ##### duplicates[​](#duplicates "Direct link to duplicates") Get the items in the collection that have duplicate values. ```php $duplicates = collect( [ 1, 2, 2, 3, 3, 3 ] )->duplicates(); // [2, 3] ``` ##### duplicates\_strict[​](#duplicates_strict "Direct link to duplicates_strict") Get the items in the collection that have duplicate values. ```php $duplicates = collect( [ 1, 2, 2, 3, 3, 3 ] )->duplicates_strict(); // [2, 3] ``` ##### each[​](#each "Direct link to each") Iterate over the items in the collection. ```php collect( [ 1, 2, 3 ] )->each( function( $item ) { // ... } ); ``` ##### each\_spread[​](#each_spread "Direct link to each_spread") Iterate over the collection's items, passing each nested item value into the given callback: ```php collect( [ [ 'John', 30 ], [ 'Jane', 25 ] ] )->each_spread( function( $name, $age ) { // ... } ); ``` ##### empty[​](#empty "Direct link to empty") Create a new, empty collection instance. ```php $empty = Collection::empty(); ``` ##### every[​](#every "Direct link to every") Determine if all items in the collection pass the given truth test. ```php $every = collect( [ 1, 2, 3 ] )->every( function( $item ) { return $item > 0; } ); ``` ##### except[​](#except "Direct link to except") Get all items except for those with the specified keys. ```php $collection = collect( [ 'name' => 'John', 'age' => 30 ] )->except( 'age' ); // ['name' => 'John'] ``` ##### filter[​](#filter "Direct link to filter") Filter the items in the collection using the given callback. ```php $collection = collect( [ 1, 2, 3 ] )->filter( function( $item ) { return $item > 1; } ); ``` ##### first[​](#first "Direct link to first") Get the first item from the collection. ```php $item = collect( [ 1, 2, 3 ] )->first(); ``` ##### first\_or\_fail[​](#first_or_fail "Direct link to first_or_fail") Get the first item in the collection that passes the given test, or throw an exception if no matching item is found. ```php use Mantle\Support\Item_Not_Found_Exception; try { $item = collect( [1, 2, 3] )->first_or_fail( fn( $item ) => $item > 2 ); } catch ( Item_Not_Found_Exception $e ) { // Handle not found } ``` ##### first\_where[​](#first_where "Direct link to first_where") Get the first item from the collection where the given key / value pair is true. ```php $item = collect( [ 'name' => 'John', 'age' => 30 ] )->first_where( 'age', '>', 25 ); ``` ##### flat\_map[​](#flat_map "Direct link to flat_map") Map a collection and flatten the result by a single level. ```php $collection = collect( [ [ 1, 2 ], [ 3, 4 ] ] )->flat_map( function( array $values ) { return array_map( function( $value ) { return $value * 2; }, $values ); } ); ``` ##### flatten[​](#flatten "Direct link to flatten") Flatten a multi-dimensional collection into a single dimension. ```php $collection = collect( [ [ 1, 2 ], [ 3, 4 ] ] )->flatten(); ``` ##### flip[​](#flip "Direct link to flip") Flip the items in the collection. ```php $collection = collect( [ 'name' => 'John', 'age' => 30 ] )->flip(); ``` ##### for\_page[​](#for_page "Direct link to for_page") Get a new collection containing the items for a given page. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->for_page( 2, 2 ); // [3, 4] ``` ##### forget[​](#forget "Direct link to forget") Remove an item from the collection by key. ```php $collection = collect( [ 'name' => 'John', 'age' => 30 ] )->forget( 'age' ); ``` ##### get[​](#get "Direct link to get") Get an item from the collection. ```php $item = collect( [ 'name' => 'John', 'age' => 30 ] )->get( 'name' ); ``` ##### get\_caching\_iterator[​](#get_caching_iterator "Direct link to get_caching_iterator") Get a CachingIterator instance for the collection, which can be useful for advanced iteration scenarios. ```php $iterator = collect( [1, 2, 3] )->get_caching_iterator(); ``` ##### group\_by[​](#group_by "Direct link to group_by") Group an associative array by a field or using a callback. ```php $collection = collect( [ [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->group_by( 'age' ); ``` ##### has[​](#has "Direct link to has") Determine if an item exists in the collection by key. ```php $has = collect( [ 'name' => 'John', 'age' => 30 ] )->has( 'name' ); ``` ##### implode[​](#implode "Direct link to implode") Join the items in the collection. ```php $joined = collect( [ 'name', 'age' ] )->implode( ', ' ); ``` ##### implode\_str[​](#implode_str "Direct link to implode_str") Join the items in the collection into a string and return a Stringable instance. ```php $string = collect( [ 'a', 'b', 'c' ] )->implode_str( ', ' ); // Stringable: 'a, b, c' ``` ##### intersect[​](#intersect "Direct link to intersect") Intersect the collection with the given items. ```php ... = collect( [ 1, 2, 3 ] )->intersect( [ 2, 3, 4 ] ); ``` ##### intersect\_assoc[​](#intersect_assoc "Direct link to intersect_assoc") Intersect the collection with the given items, key and value-wise. ```php $intersect = collect( [ 'name' => 'John' ] )->intersect_assoc( [ 'name' => 'Jane' ] ); ``` ##### intersect\_by\_keys[​](#intersect_by_keys "Direct link to intersect_by_keys") Intersect the collection with the given items by key. ```php $intersect = collect( [ 'name' => 'John' ] )->intersect_by_keys( [ 'name' => 'Jane' ] ); ``` ##### is\_empty[​](#is_empty "Direct link to is_empty") Determine if the collection is empty. ```php $isEmpty = collect( [] )->is_empty(); ``` ##### is\_not\_empty[​](#is_not_empty "Direct link to is_not_empty") Determine if the collection is not empty. ```php $isNotEmpty = collect( [ 1, 2, 3 ] )->is_not_empty(); ``` ##### join[​](#join "Direct link to join") Join the items in the collection. ```php $joined = collect( [ 'name', 'age' ] )->join( ', ' ); ``` ##### key\_by[​](#key_by "Direct link to key_by") Key an associative array by a field or using a callback. ```php $collection = collect( [ [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->key_by( 'name' ); ``` ##### keys[​](#keys "Direct link to keys") Get the keys of the collection items. ```php $keys = collect( [ 'name' => 'John', 'age' => 30 ] )->keys(); ``` ##### last[​](#last "Direct link to last") Get the last item from the collection. ```php $item = collect( [ 1, 2, 3 ] )->last(); ``` ##### make[​](#make "Direct link to make") Create a new collection instance from the given items. ```php $collection = Collection::make( [1, 2, 3] ); ``` ##### map[​](#map "Direct link to map") Map the items in the collection to a new callback. ```php $collection = collect( [ 1, 2, 3 ] )->map( function( $item ) { return $item * 2; } ); ``` ##### map\_into[​](#map_into "Direct link to map_into") Map the collection into a new class. ```php $collection = collect( [ 'John', 'Jane' ] )->map_into( User::class ); ``` ##### map\_spread[​](#map_spread "Direct link to map_spread") Iterates over the collection's items, passing each nested item value into the given callback: ```php $collection = collect( [ [ 'John', 30 ], [ 'Jane', 25 ] ] )->map_spread( function( $name, $age ) { return $name . ' is ' . $age . ' years old'; } ); ``` ##### map\_to\_dictionary[​](#map_to_dictionary "Direct link to map_to_dictionary") Map the collection to a dictionary. ```php $dictionary = collect( [ [ 'name' => 'John', 'department' => 'Sales', ], [ 'name' => 'Jane', 'department' => 'Marketing', ], [ 'name' => 'Jack', 'department' => 'Sales', ], ] )->map_to_dictionary( function ( $item ) { return [ $item['department'] => $item['name'], ]; } ); // [ 'Marketing' => ['John', 'Jane'], 'Sales' => ['Jack']] ``` You can also return false/null to skip an item. ```php $dictionary = collect( [ [ 'name' => 'John', 'department' => 'Sales', ], [ 'name' => 'Jane', 'department' => 'Marketing', ], [ 'name' => 'Jack', 'department' => 'Sales', ], [ 'name' => 'Adam', 'department' => 'Tech', ], ] ] )->map_to_dictionary( function ( $item ) { if ( $item['department'] === 'Sales' ) { return false; } return [ $item['department'] => $item['name'], ]; } ); // ['Marketing' => ['Jane'], 'Tech' => ['Adam']] ``` ##### map\_to\_groups[​](#map_to_groups "Direct link to map_to_groups") Alias to `map_to_dictionary`. ##### map\_with\_keys[​](#map_with_keys "Direct link to map_with_keys") Map the collection with the given key / value pairs. ```php $collection = collect( [ [ 'name' => 'John', 'department' => 'Sales', 'email' => 'john@example.com', ], [ 'name' => 'Jane', 'department' => 'Marketing', 'email' => 'jane@example.com', ] ] ); $collection = $collection->map_with_keys( function( $item ) { return [ $item['name'] => $item['email'] ]; } ); ``` ##### max[​](#max "Direct link to max") Get the max value of a given key. ```php $max = collect( [ 1, 2, 3 ] )->max(); ``` ##### median[​](#median "Direct link to median") Get the median value of a given key. ```php $median = collect( [ 1, 2, 3 ] )->median(); ``` ##### merge[​](#merge "Direct link to merge") Merge the collection with the given items. ```php $collection = collect( [ 'name' => 'John' ] )->merge( [ 'name' => 'Jane' ] ); ``` ##### merge\_recursive[​](#merge_recursive "Direct link to merge_recursive") Merge the collection with the given items, recursively. ```php $collection = collect( [ 'name' => 'John' ] )->merge_recursive( [ 'name' => 'Jane' ] ); ``` ##### min[​](#min "Direct link to min") Get the min value of a given key. ```php $min = collect( [ 1, 2, 3 ] )->min(); ``` ##### mode[​](#mode "Direct link to mode") Get the mode value of a given key. ```php $mode = collect( [ 1, 2, 2, 3, 3, 3 ] )->mode(); ``` ##### nth[​](#nth "Direct link to nth") Create a new collection consisting of every n-th element. ```php $collection = collect( [ 1, 2, 3, 4, 5, 6 ] )->nth( 2 ); // [2, 4, 6] ``` ##### only[​](#only "Direct link to only") Create a new collection consisting of the given keys. ```php $collection = collect( [ 'name' => 'John', 'department' => 'Sales', ] )->only( [ 'name' ] ); ``` ##### only\_children[​](#only_children "Direct link to only_children") Create a new collection consisting of only keys that are children of the given keys. ```php $collection = collect( [ [ 'name' => 'John', 'department' => 'Sales', 'email' => 'john@example.com', ], [ 'name' => 'Jane', 'department' => 'Marketing', 'email' => 'jane@example.com', ] ] ); $collection = $collection->only_children( [ 'email', 'name' ] ); // [['name' => 'John', 'email' => 'john@example.com'], ['name' => 'Jane', 'email' => 'jane@example.com']] ``` ##### pad[​](#pad "Direct link to pad") Pad the collection to the specified size with a value. ```php $collection = collect( [ 1, 2, 3 ] )->pad( 5, 0 ); // [1, 2, 3, 0, 0] ``` ##### partition[​](#partition "Direct link to partition") Partition the collection into two arrays using the given callback or key. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->partition( function( $value, $key ) { return $value > 2; } ); ``` ##### pipe[​](#pipe "Direct link to pipe") Pass the collection to the given callback and return the result. ```php $sum = collect( [1, 2, 3] )->pipe( fn( $collection ) => $collection->sum() ); // 6 ``` ##### pipe\_into[​](#pipe_into "Direct link to pipe_into") Pass the collection into a new class instance. ```php class Stats { public function __construct( $collection ) { $this->total = $collection->sum(); } } $stats = collect( [1, 2, 3] )->pipe_into( Stats::class ); // $stats->total === 6 ``` ##### pipe\_through[​](#pipe_through "Direct link to pipe_through") Pass the collection through a series of callbacks and return the result. ```php $result = collect( [1, 2, 3] )->pipe_through([ fn( $c ) => $c->map( fn( $i ) => $i * 2 ), fn( $c ) => $c->sum(), ]); // 12 ``` ##### pluck[​](#pluck "Direct link to pluck") Get an array with the values of a given key. ```php $plucked = collect( [ [ 'name' => 'John', 'department' => 'Sales' ], [ 'name' => 'Jane', 'department' => 'Marketing' ], ] )->pluck( 'name' ); ``` ##### pop[​](#pop "Direct link to pop") Remove and return the last item from the collection. ```php $item = collect( [ 1, 2, 3 ] )->pop(); // 3 ``` ##### prepend[​](#prepend "Direct link to prepend") Prepend an item to the beginning of the collection. ```php $collection = collect( [ 1, 2, 3 ] )->prepend( 0 ); ``` ##### prepend\_many[​](#prepend_many "Direct link to prepend_many") Prepend multiple items to the beginning of the collection. ```php $collection = collect( [ 1, 2, 3 ] )->prepend_many( [ 0, -1, -2 ] ); // [0, -1, -2, 1, 2, 3] ``` ##### pull[​](#pull "Direct link to pull") Removes and returns an item from the collection by key. ```php $item = collect( [ 'name' => 'John' ] )->pull( 'name' ); ``` ##### push[​](#push "Direct link to push") Push an item onto the end of the collection. ```php $collection = collect( [ 1, 2, 3 ] )->push( 4 ); ``` ##### put[​](#put "Direct link to put") Put an item in the collection by key. ```php $collection = collect( [ 'name' => 'John' ] )->put( 'age', 30 ); ``` ##### random[​](#random "Direct link to random") Return a random item from the collection. ```php $random = collect( [ 1, 2, 3 ] )->random(); ``` ##### reduce[​](#reduce "Direct link to reduce") Reduce the collection to a single value. ```php $reduced = collect( [ 1, 2, 3 ] )->reduce( function( $carry, $item ) { return $carry + $item; } ); ``` ##### reduce\_spread[​](#reduce_spread "Direct link to reduce_spread") Reduce the collection to multiple aggregate values at once. ```php [$min, $max] = collect( [1, 2, 3] )->reduce_spread( function( $min, $max, $value ) { return [ min( $min, $value ), max( $max, $value ), ]; }, PHP_INT_MAX, PHP_INT_MIN ); // $min = 1, $max = 3 ``` ##### reject[​](#reject "Direct link to reject") Filter items in the collection using the given callback. ```php $collection = collect( [ 1, 2, 3 ] )->reject( function( $item ) { return $item > 1; } ); ``` ##### replace[​](#replace "Direct link to replace") Replace items in the collection with the given items. ```php $collection = collect( [ 'name' => 'John' ] )->replace( [ 'name' => 'Jane' ] ); ``` ##### replace\_recursive[​](#replace_recursive "Direct link to replace_recursive") Replace items in the collection with the given items, recursively. ```php $collection = collect( [ 'name' => 'John' ] )->replace_recursive( [ 'name' => 'Jane' ] ); ``` ##### reverse[​](#reverse "Direct link to reverse") Reverse items in the collection. ```php $collection = collect( [ 1, 2, 3 ] )->reverse(); ``` ##### search[​](#search "Direct link to search") Search the collection for a given value and return the corresponding key if successful. ```php $key = collect( [ 'name' => 'John' ] )->search( 'John' ); ``` ##### shift[​](#shift "Direct link to shift") Remove and return the first item from the collection. ```php $item = collect( [ 1, 2, 3 ] )->shift(); // 1 ``` ##### shuffle[​](#shuffle "Direct link to shuffle") Shuffle the items in the collection. ```php $collection = collect( [ 1, 2, 3 ] )->shuffle(); ``` ##### skip[​](#skip "Direct link to skip") Skip the first n items. ```php $collection = collect( [ 1, 2, 3 ] )->skip( 2 ); ``` ##### skip\_until[​](#skip_until "Direct link to skip_until") Skip items in the collection until the given condition is met. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->skip_until( function( $value ) { return $value >= 3; } ); // [3, 4, 5] $collection = collect( [ 1, 2, 3, 4, 5 ] )->skip_until( 3 ); // [3, 4, 5] ``` ##### skip\_while[​](#skip_while "Direct link to skip_while") Skip items in the collection while the given condition is met. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->skip_while( function( $value ) { return $value <= 3; } ); // [4, 5] $collection = collect( [ 1, 2, 3, 4, 5 ] )->skip_while( function( $value ) { return $value < 3; } ); // [3, 4, 5] ``` ##### slice[​](#slice "Direct link to slice") Slice the collection by the given offset and length. ```php $collection = collect( [ 1, 2, 3 ] )->slice( 1, 2 ); // [2, 3] ``` ##### sliding[​](#sliding "Direct link to sliding") Create a "sliding window" view of the items in the collection. ```php $windows = collect( [1, 2, 3, 4] )->sliding( 2 ); // [[1, 2], [2, 3], [3, 4]] ``` ##### sole[​](#sole "Direct link to sole") Get the first item in the collection, but only if exactly one item exists. Throws an exception otherwise. ```php use Mantle\Support\Item_Not_Found_Exception; use Mantle\Support\Multiple_Items_Found_Exception; try { $item = collect( [42] )->sole(); } catch ( Item_Not_Found_Exception | Multiple_Items_Found_Exception $e ) { // Handle error } ``` ##### some[​](#some "Direct link to some") Alias for the `contains` method. ##### sort[​](#sort "Direct link to sort") Sort the collection with an optional callback. ```php $collection = collect( [ 3, 2, 1 ] )->sort(); ``` ##### sort\_by[​](#sort_by "Direct link to sort_by") Sort the collection by the given callback. ```php $collection = collect( [ 1, 3, 2 ] )->sort_by( function( $item ) { return $item; } ); ``` ##### sort\_by\_desc[​](#sort_by_desc "Direct link to sort_by_desc") Sort the collection by the given callback in descending order. ```php $collection = collect( [ 3, 2, 1 ] )->sort_by_desc( function( $item ) { return $item; } ); ``` ##### sort\_desc[​](#sort_desc "Direct link to sort_desc") Sort the collection in descending order. ```php $collection = collect( [ 3, 2, 1 ] )->sort_desc(); ``` ##### sort\_keys[​](#sort_keys "Direct link to sort_keys") Sort the collection by keys. ```php $collection = collect( [ 'b' => 2, 'a' => 1 ] )->sort_keys(); ``` ##### sort\_keys\_desc[​](#sort_keys_desc "Direct link to sort_keys_desc") Sort the collection by keys in descending order. ```php $collection = collect( [ 'b' => 2, 'a' => 1 ] )->sort_keys_desc(); ``` ##### sort\_keys\_using[​](#sort_keys_using "Direct link to sort_keys_using") Sort the collection keys using a custom callback. ```php $collection = collect( [ 'b' => 2, 'a' => 1 ] )->sort_keys_using( function( $a, $b ) { return strcmp( $a, $b ); } ); // ['a' => 1, 'b' => 2] ``` ##### splice[​](#splice "Direct link to splice") Remove and return a portion of the collection. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->splice( 1, 2 ); ``` ##### split[​](#split "Direct link to split") Split the collection into the given number of groups. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->split( 2 ); // [[1, 2, 3], [4, 5]] ``` ##### split\_in[​](#split_in "Direct link to split_in") Split the collection into the given number of groups, filling the first groups completely. ```php $groups = collect( [1, 2, 3, 4, 5] )->split_in( 2 ); // [[1, 2, 3], [4, 5]] ``` ##### sum[​](#sum "Direct link to sum") Get the sum of the given values. ```php $sum = collect( [ 1, 2, 3 ] )->sum(); $sum = collect( [ [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->sum( 'age' ); ``` ##### take[​](#take "Direct link to take") Take the first or last n items. ```php $collection = collect( [ 1, 2, 3 ] )->take( 2 ); ``` ##### take\_until[​](#take_until "Direct link to take_until") Take items in the collection until the given condition is met. Stops before the item that matches the condition. You can pass a value or a callback. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->take_until( function( $value ) { return $value >= 4; } ); // [1, 2, 3] $collection = collect( [ 1, 2, 3, 4, 5 ] )->take_until( 3 ); // [1, 2] ``` ##### take\_while[​](#take_while "Direct link to take_while") Take items in the collection while the given condition is met. You can pass a value or a callback. ```php $collection = collect( [ 1, 2, 3, 4, 5 ] )->take_while( function( $value ) { return $value < 4; } ); // [1, 2, 3] $collection = collect( [ 1, 1, 2, 3, 2 ] )->take_while( 1 ); // [1, 1] ``` ##### tap[​](#tap "Direct link to tap") Pass the collection to the given callback and then return the collection. ```php $collection = collect( [1, 2, 3] )->tap( function( $c ) { // Do something with $c } ); ``` ##### times[​](#times "Direct link to times") Create a new collection by invoking the callback a given number of times. ```php $collection = Collection::times( 3, fn( $i ) => $i * 2 ); // [2, 4, 6] ``` ##### to\_array[​](#to_array "Direct link to to_array") Convert the collection to a plain array. ```php $array = collect( [ 1, 2, 3 ] )->to_array(); ``` ##### to\_base[​](#to_base "Direct link to to_base") Get a base Mantle collection instance from this collection. ```php $base = $collection->to_base(); ``` ##### to\_json[​](#to_json "Direct link to to_json") Convert the collection to its JSON representation. ```php $json = collect( [ 1, 2, 3 ] )->to_json(); ``` ##### to\_pretty\_json[​](#to_pretty_json "Direct link to to_pretty_json") Get the collection as pretty-printed JSON. ```php $json = collect( [1, 2, 3] )->to_pretty_json(); ``` ##### toArray[​](#toarray "Direct link to toArray") Alias for the `to_array` methods. ##### transform[​](#transform "Direct link to transform") Transform each item in the collection using a callback (modifies the collection in place). ```php $collection = collect( [1, 2, 3] )->transform( fn( $item ) => $item * 2 ); // [2, 4, 6] ``` ##### trim[​](#trim "Direct link to trim") Trim the collection's string values. ```php $collection = collect( [ ' John ', ' Jane ' ] )->trim(); ``` ##### undot[​](#undot "Direct link to undot") Expand a flattened "dot" notation array into an expanded array. ```php $collection = collect( [ 'user.name' => 'John' ] )->undot(); // [ 'user' => [ 'name' => 'John' ] ] ``` ##### unique[​](#unique "Direct link to unique") Filter the collection for unique items. ```php $collection = collect( [ 1, 2, 2, 3, 3, 3 ] )->unique(); ``` ##### unique\_strict[​](#unique_strict "Direct link to unique_strict") Filter the collection with a strict comparison. ```php $collection = collect( [ 1, 2, 2, 3, 3, 3 ] )->unique_strict(); ``` ##### unless\_empty[​](#unless_empty "Direct link to unless_empty") Execute a callback if the collection is not empty. ```php $collection = collect( [ 1, 2, 3 ] )->unless_empty( function( $collection ) { // ... } ); ``` ##### unless\_not\_empty[​](#unless_not_empty "Direct link to unless_not_empty") Execute a callback if the collection is empty. ```php $collection = collect( [] )->unless_not_empty( function( $collection ) { // ... } ); ``` ##### unwrap[​](#unwrap "Direct link to unwrap") Get the underlying array from a collection or arrayable value. ```php $array = Collection::unwrap( collect( [1, 2, 3] ) ); // [1, 2, 3] ``` ##### values[​](#values "Direct link to values") Get the values of the collection items. ```php $values = collect( [ 'name' => 'John', 'age' => 30 ] )->values(); ``` ##### when\_empty[​](#when_empty "Direct link to when_empty") Execute a callback if the collection is empty. ```php $collection = collect( [] )->when_empty( function( $collection ) { // ... } ); ``` ##### when\_not\_empty[​](#when_not_empty "Direct link to when_not_empty") Execute a callback if the collection is not empty. ```php $collection = collect( [ 1, 2, 3 ] )->when_not_empty( function( $collection ) { // ... } ); ``` ##### where[​](#where "Direct link to where") Filter the collection by determining if a specific key / value pair is true. ```php $collection = collect( [ 'name' => 'John' ] )->where( 'name', 'John' ); $collection = collect( [ 'name' => 'John' ] )->where( 'name', '!=', 'John' ); ``` ##### where\_between[​](#where_between "Direct link to where_between") Filter the collection by determining if a specific item is between two values. ```php $collection = collect( [ [ 'name' => 'Jack', 'age' => 35 ], [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->where_between( 'age', [ 25, 30 ] ); ``` ##### where\_in[​](#where_in "Direct link to where_in") Filter the collection by determining if a specific key / value pair is in the given array. ```php $collection = collect( [ 'name' => 'John' ] )->where_in( 'name', [ 'John' ] ); ``` ##### where\_in\_strict[​](#where_in_strict "Direct link to where_in_strict") Filter the collection by determining if a specific key / value pair is in the given array. ```php $collection = collect( [ 'name' => 'John' ] )->where_in_strict( 'name', [ 'John' ] ); ``` ##### where\_instance\_of[​](#where_instance_of "Direct link to where_instance_of") Filter the collection by determining if a specific item is an instance of the given class. ```php $collection = collect( [ new User, new Post ] )->where_instance_of( User::class ); ``` ##### where\_not\_between[​](#where_not_between "Direct link to where_not_between") Filter the collection by determining if a specific item is not between two values. ```php $collection = collect( [ [ 'name' => 'Jack', 'age' => 35 ], [ 'name' => 'John', 'age' => 30 ], [ 'name' => 'Jane', 'age' => 25 ], ] )->where_not_between( 'age', [ 25, 30 ] ); ``` ##### where\_not\_in[​](#where_not_in "Direct link to where_not_in") Filter the collection by determining if a specific key / value pair is not in the given array. ```php $collection = collect( [ 'name' => 'John' ] )->where_not_in( 'name', [ 'Jane' ] ); ``` ##### where\_not\_in\_strict[​](#where_not_in_strict "Direct link to where_not_in_strict") Filter the collection by determining if a specific key / value pair is not in the given array. ```php $collection = collect( [ 'name' => 'John' ] )->where_not_in_strict( 'name', [ 'Jane' ] ); ``` ##### where\_not\_null[​](#where_not_null "Direct link to where_not_null") Filter the collection by determining if a specific key / value pair is not null. ```php $collection = collect( [ 'name' => 'John' ] )->where_not_null( 'name' ); ``` ##### where\_null[​](#where_null "Direct link to where_null") Filter the collection by determining if a specific key / value pair is null. ```php $collection = collect( [ 'name' => null ] )->where_null( 'name' ); ``` ##### where\_strict[​](#where_strict "Direct link to where_strict") Filter the collection by determining if a specific key / value pair is true. ```php $collection = collect( [ 'name' => 'John' ] )->where_strict( 'name', 'John' ); ``` ##### wrap[​](#wrap "Direct link to wrap") Wrap the given value in a collection if it isn't already one. ```php $collection = Collection::wrap( 5 ); // [5] ``` Pass the collection to the given closure and returns the result of the executed closure. ```php $collection = collect( [ 1, 2, 3 ] ); $piped = $collection->pipe( function ( Collection $collection ) { return $collection->sum(); } ); // 6 ``` ##### zip[​](#zip "Direct link to zip") The `zip` method merges together the values of the given array with the values of the original collection at their corresponding index: --- ### Conditionable Conditionable is a trait that provides a way to conditionally call a closure based on a condition. ##### Usage[​](#usage "Direct link to Usage") To use the `Conditionable` trait, you can use the following pattern: ```php namespace App; use Mantle\Support\Traits\Conditionable; class Example_Class { use Conditionable; public function __construct( public int $number = 1, ) {} public function double(): static { $this->number *= 2; return $this; } // Your class code here. } ``` Then you can conditionally call a closure like this: ```php $instance = new Example_Class(); // If the number is greater than 10, call the closure and double the number. $instance->when( $instance->number > 10, function() { $this->number *= 2; }); ``` The closure will only be called if the condition is true. You can also pass a second closure to be called if the condition is false: ```php $instance = new Example_Class(); // If the number is greater than 10, call the first closure and double the number. // If the number is less than or equal to 10, call the second closure and triple the number. $instance->when( $instance->number > 10, function() { $this->number *= 2; }, function() { $this->number *= 3; }); ``` --- ### Helpers General helpers for development. #### Array[​](#array "Direct link to Array") ##### `data_get`[​](#data_get "Direct link to data_get") The `data_get` helper retrieves a value from an array or object using "dot" notation. If the specified key does not exist, it will return `null` or a default value if provided. ```php use function Mantle\Support\Helpers\data_get; $data = [ 'user' => [ 'name' => 'John Doe', 'email' => 'example@example.com' ] ]; $email = data_get( $data, 'user.email' ); // returns 'example@example.com ``` ##### `data_set`[​](#data_set "Direct link to data_set") The `data_set` helper sets a value in an array or object using "dot" notation. If the specified key does not exist, it will be created. ```php use function Mantle\Support\Helpers\data_set; $data = [ 'user' => [ 'name' => 'John Doe', ], ]; data_set( $data, 'user.email', 'example@example.com' ); ``` ##### `data_fill`[​](#data_fill "Direct link to data_fill") The `data_fill` helper is used to set a value in an array or object using "dot" notation. If the specified key does not exist, it not be created. ```php use function Mantle\Support\Helpers\data_fill; $data = [ 'user' => [ 'name' => 'John Doe', ], ]; data_fill( $data, 'user.email', 'example@example.com' ); ``` ##### `head`[​](#head "Direct link to head") The `head` helper retrieves the first element of an array. If the array is empty, it will return `null`. ```php use function Mantle\Support\Helpers\head; $array = [ 'apple', 'banana', 'cherry' ]; $first = head( $array ); // returns 'apple' ``` ##### `last`[​](#last "Direct link to last") The `last` helper retrieves the last element of an array. If the array is empty, it will return `null`. ```php use function Mantle\Support\Helpers\last; $array = [ 'apple', 'banana', 'cherry' ]; $last = last( $array ); // returns 'cherry' ``` ##### `value`[​](#value "Direct link to value") The `value` helper retrieves the value of a variable or calls a callback if the variable is a closure. This is useful for lazy evaluation. ```php use function Mantle\Support\Helpers\value; $value = value( 'Hello, World!' ); // returns 'Hello, World!' $value = value( fn () => { return 'Hello, World!'; } ); // returns 'Hello, World!' ``` #### Core Objects[​](#core-objects "Direct link to Core Objects") ##### `get_comment_object`[​](#get_comment_object "Direct link to get_comment_object") Nullable wrapper for `get_comment()`. ```php use function Mantle\Support\Helpers\get_comment_object; $comment = get_comment_object( 1 ); // returns a comment object or null if not found ``` ##### `get_post_object`[​](#get_post_object "Direct link to get_post_object") Nullable wrapper for `get_post()`. ```php use function Mantle\Support\Helpers\get_post_object; $post = get_post_object( 1 ); // returns a post object or null if not found ``` ##### `get_site_object`[​](#get_site_object "Direct link to get_site_object") Nullable wrapper for `get_site()`. ```php use function Mantle\Support\Helpers\get_site_object; $site = get_site_object( 1 ); // returns a site object or null if not found ``` ##### `get_term_object`[​](#get_term_object "Direct link to get_term_object") Nullable wrapper for `get_term()`. ```php use function Mantle\Support\Helpers\get_term_object; $term = get_term_object( 1 ); // returns a term object or null if not found ``` ##### `get_term_object_by`[​](#get_term_object_by "Direct link to get_term_object_by") Nullable wrapper for `get_term_by()`. ```php use function Mantle\Support\Helpers\get_term_object_by; $term = get_term_object_by( 'slug', 'example-slug', 'category' ); // returns a term object or null if not found ``` ##### `get_user_object`[​](#get_user_object "Direct link to get_user_object") Nullable wrapper for `get_user()`. ```php use function Mantle\Support\Helpers\get_user_object; $user = get_user_object( 1 ); // returns a user object or null if not found ``` ##### `get_user_object_by`[​](#get_user_object_by "Direct link to get_user_object_by") Nullable wrapper for `get_user_by()`. ```php use function Mantle\Support\Helpers\get_user_object_by; $user = get_user_object_by( 'login', 'exampleuser' ); // returns a user object or null if not found ``` #### Environment[​](#environment "Direct link to Environment") ##### `is_hosted_env`[​](#is_hosted_env "Direct link to is_hosted_env") The `is_hosted_env` helper checks if the application is running in a hosted environment. Checks if the site is a `production` environment. ```php use function Mantle\Support\Helpers\is_hosted_env; if ( is_hosted_env() ) { // The application is running in a hosted environment } ``` ##### `is_local_env`[​](#is_local_env "Direct link to is_local_env") The `is_local_env` helper checks if the application is running in a local environment. Checks if the site is a `local` environment. ```php use function Mantle\Support\Helpers\is_local_env; if ( is_local_env() ) { // The application is running in a local environment } ``` ##### `is_wp_cli`[​](#is_wp_cli "Direct link to is_wp_cli") The `is_wp_cli` helper checks if the application is running in the WP-CLI context. This is useful for determining if the code is being executed through the command line interface. ```php use function Mantle\Support\Helpers\is_wp_cli; if ( is_wp_cli() ) { // The application is running in the WP-CLI context } ``` ##### `is_unit_testing`[​](#is_unit_testing "Direct link to is_unit_testing") The `is_unit_testing` helper checks if the application is running in a unit testing environment via the [testing framework](/docs/testing.md). ```php use function Mantle\Support\Helpers\is_unit_testing; if ( is_unit_testing() ) { // The application is running in a unit testing environment dump( 'Unit testing is enabled!' ); } ``` #### General[​](#general "Direct link to General") ##### `backtickit`[​](#backtickit "Direct link to backtickit") The `backtickit` helper is a simple utility to wrap a string in backticks (\`\`\`). ```php use function Mantle\Support\Helpers\backtickit; $wrapped = backtickit( 'example' ); // returns '`example`' ``` ##### `blank`[​](#blank "Direct link to blank") The `blank` helper checks if a value is "blank". A value is considered blank if it is `null`, an empty string, or an empty array. ```php use function Mantle\Support\Helpers\blank; blank( null ); // returns true blank( '' ); // returns true blank( [] ); // returns true blank( 'example' ); // returns false ``` ##### `class_basename`[​](#class_basename "Direct link to class_basename") The `class_basename` helper retrieves the class name of a given object or class name without the namespace. ```php use function Mantle\Support\Helpers\class_basename; class_basename( \Mantle\Support\Helpers\ExampleClass::class ); // returns 'ExampleClass' ``` ##### `class_uses_recursive`[​](#class_uses_recursive "Direct link to class_uses_recursive") The `class_uses_recursive` helper returns all traits used by a class, its parent classes and trait of their traits. ```php use function Mantle\Support\Helpers\class_uses_recursive; class_uses_recursive( \Mantle\Support\Helpers\ExampleClass::class ); // returns an array of trait names ``` ##### `classname`[​](#classname "Direct link to classname") See [Classname](/docs/features/support/classname.md); ##### `collect`[​](#collect "Direct link to collect") Returns a [Collection](/docs/features/support/collections.md) instance containing the given value. ```php use function Mantle\Support\Helpers\collect; $collection = collect( [ 1, 2, 3 ] ); // returns a Collection instance ``` ##### `defer`[​](#defer "Direct link to defer") The `defer()` helper function can be used to defer the execution of a function until the end of the request lifecycle. This can be useful for deferring functions that should be executed after the response has been sent to the user. ```php use function Mantle\Support\Helpers\defer; defer( function() { // Your deferred function code here. } ); ``` ##### `filled`[​](#filled "Direct link to filled") The `filled` helper checks if a value is "filled". A value is considered filled if it is not `null`, an empty string, or an empty array. ```php use function Mantle\Support\Helpers\filled; filled( null ); // returns false filled( '' ); // returns false filled( [] ); // returns false filled( 'example' ); // returns true ``` ##### `get_callable_fqn`[​](#get_callable_fqn "Direct link to get_callable_fqn") The `get_callable_fqn` helper retrieves the fully qualified name of a callable or a closure. This is useful for debugging or logging purposes. ##### `html_string`[​](#html_string "Direct link to html_string") Returns a [HTML instance](/docs/features/support/html.md) containing the given value. ```php use function Mantle\Support\Helpers\html_string; $html = html_string( '
Hello World
' ); ``` ##### `object_get`[​](#object_get "Direct link to object_get") The `object_get` helper retrieves a value from an object using "dot" notation. If the specified key does not exist, it will return `null` or a default value if provided. ```php use function Mantle\Support\Helpers\object_get; $object = (object) [ 'user' => (object) [ 'name' => 'John Doe', ] ]; $name = object_get( $object, 'user.name' ); // returns 'John Doe' ``` ##### `now`[​](#now "Direct link to now") The `now` helper returns an instance of `Mantle\Support\Carbon` representing the current date and time. ```php use function Mantle\Support\Helpers\now; $current_time = now(); // returns current date and time as a Carbon instance $future = now()->addDays( 10 ); // returns date and time 10 days from now ``` You can mock the current time using [Time Mocking](/docs/testing/helpers.md#time-mocking) when testing. ##### `retry`[​](#retry "Direct link to retry") The `retry` helper attempts to execute a given callback a specified number of times until it succeeds or the maximum number of attempts is reached. If the callback fails, it will wait for a specified delay before retrying. ```php use function Mantle\Support\Helpers\retry; retry( times: 5 callback: function() { // Your code that may fail }, sleep: 100 // milliseconds ``` ##### `stringable`[​](#stringable "Direct link to stringable") Returns an instance of `Mantle\Support\Str` which contains all the Laravel string helper methods you might be familiar with. You can reference those [here](https://laravel.com/docs/11.x/helpers#strings). ```php use function Mantle\Support\Helpers\stringable; stringable( 'example string' )->title(); // Example String ``` ##### `tap`[​](#tap "Direct link to tap") The `tap` helper allows you to pass a value to a callback and then return the original value. This is useful for performing side effects on a value without modifying it. ```php use function Mantle\Support\Helpers\tap; $value = tap( new Example(), function ( Example $example ) { $example->doSomething(); }, ); ``` ##### `throw_if`[​](#throw_if "Direct link to throw_if") The `throw_if` helper throws an exception if the given condition passes. This is useful for validating conditions before executing code. ```php use function Mantle\Support\Helpers\throw_if; throw_if( $value_to_check, \Exception::class, 'Invalid value!' ); ``` ##### `throw_unless`[​](#throw_unless "Direct link to throw_unless") The `throw_unless` helper throws an exception if the given condition fails. This is useful for validating conditions before executing code. ```php use function Mantle\Support\Helpers\throw_unless; throw_unless( $value_to_check, \Exception::class, 'Invalid value!' ); ``` ##### `trait_uses_recursive`[​](#trait_uses_recursive "Direct link to trait_uses_recursive") The `trait_uses_recursive` helper returns all traits used by a trait and its traits. ```php use function Mantle\Support\Helpers\trait_uses_recursive; trait_uses_recursive( \Mantle\Support\Helpers\ExampleTrait::class ); // returns an array of trait names ``` ##### `with`[​](#with "Direct link to with") The `with` helper returns the given value, optionally passed through the given callback. ```php use function Mantle\Support\Helpers\with; $value = with( 'example', function( $value ) { return strtoupper( $value ); } ); // returns 'EXAMPLE' ``` ##### `memo`[​](#memo "Direct link to memo") The `memo` helper memoizes the result of a callback function, caching it to avoid repeated execution. It works similar to Spatie's `once` helper but with the added ability to specify dependencies that determine when the cached value should be invalidated (think React's `useMemo`). ```php use function Mantle\Support\Helpers\memo; // Basic usage - function will only run once, subsequent calls return the cached value. $result = memo( function() { // Expensive operation that will only execute once return expensive_calculation(); } ); // With dependencies - function result is cached based on the dependency values. $post_content = memo( function() use ( $post ) { // This will be recalculated only when $post->ID changes return apply_filters( 'the_content', $post->post_content ); }, [ $post->ID ] // Dependencies array ); // Multiple dependencies can be specified $user_posts = memo( function() use ( $user_id, $post_status ) { return get_posts([ 'author' => $user_id, 'post_status' => $post_status, ]); }, [ $user_id, $post_status ] ); ``` tip Use `memo()` for expensive operations that are called multiple times but don't need to be recalculated unless dependencies change. This can significantly improve performance in your application. #### Meta[​](#meta "Direct link to Meta") Helpers for registering meta fields on objects in WordPress. ##### `register_meta_helper`[​](#register_meta_helper "Direct link to register_meta_helper") Register meta for a specific object type with some common defaults. By default, the meta will have the following settings: * `show_in_rest` is set to `true` * `single` is set to `true` * `type` is set to `string` Example: ```php use function Mantle\Support\Helpers\register_meta_helper; register_meta_helper( 'post', 'example_meta_key', [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'default' => '', ] ); ``` ##### `register_meta_from_file`[​](#register_meta_from_file "Direct link to register_meta_from_file") Register meta fields from a JSON file. The JSON file should contain an array of meta field definitions. Let's say you have a JSON file with meta definitions like this: post-meta.json ```php { "$schema": "https://raw.githubusercontent.com/alleyinteractive/mantle-framework/HEAD/src/mantle/support/schema/meta.json", "example_meta_key": { "post_types": "article", "type": "string" }, "another_meta_key": { "post_types": [ "article", "page" ], "type": "number", "single": false, "default": 0 } } ``` You can register the meta fields defined in this file using the `register_meta_from_file` helper: ```php use function Mantle\Support\Helpers\register_meta_from_file; register_meta_from_file( __DIR__ . '/post-meta.json', 'post' ); ``` #### Testing[​](#testing "Direct link to Testing") ##### `capture`[​](#capture "Direct link to capture") The `capture()` helper is a simple wrapper around `ob_start()` and `ob_get_clean()`. It captures the output of a callable and returns it as a string. This can be useful when you want to test the output of a function or method that echoes content. Example: ```php use function Mantle\Support\Helpers\capture; $output = capture( fn () => the_title() ); ``` ##### `block_factory`[​](#block_factory "Direct link to block_factory") The `block_factory()` helper creates a new instance of a block factory. This can be useful for testing or creating blocks dynamically. For more information see [the documentation](/docs/testing/factory.md#generating-blocks). ```php use function Mantle\Testing\block_factory; $block = block_factory()->block( name: 'example/block-name', attributes: [ 'example' => 'value' ], ); $heading = block_factory()->heading( 'Example Heading' ); $paragraph = block_factory()->paragraph( 'Example Paragraph' ); ``` #### Requests[​](#requests "Direct link to Requests") ##### `terminate_request`[​](#terminate_request "Direct link to terminate_request") The `terminate_request()` helper function is used to terminate the current HTTP request and send a response to the client. This is useful in scenarios where you want to stop further processing and immediately return a response. This helper also supports unit testing by not exiting the script when running a unit test. It will allow the response to be properly captured and asserted against. ```php use function Mantle\Support\Helpers\terminate_request; if ( some_condition() ) { echo 'Terminating request early.'; terminate_request( exit_status: 0, response_code: 200 ); } ``` ##### `send_json_response`[​](#send_json_response "Direct link to send_json_response") The `send_json_response()` helper function is used to send a JSON response to the client and terminate the current HTTP request. This is useful for API endpoints or AJAX requests where you want to return data in JSON format. This helper also supports unit testing by not exiting the script when running a unit test. It will allow the response to be properly captured and asserted against. ```php use function Mantle\Support\Helpers\send_json_response; $data = [ 'success' => true, 'message' => 'Data processed successfully.', ]; send_json_response( data: $data, status_code: 200 ); ``` --- ### Hookable Hookable is a trait that will automatically register methods on your class with WordPress action/filter hooks. This allows you to define your hooks in a more fluent way keeping the action/filter registration close to the method that implements the hook. Underneath it calls the core `add_action`/`add_filter` methods. tip The Hookable trait pairs well with feature classes. See [Hookable Feature](/docs/features/types.md#hookable-feature) for more information. #### Usage With Attributes[​](#usage-with-attributes "Direct link to Usage With Attributes") To use the `Hookable` trait, you can use the following pattern: ```php namespace App; use Mantle\Support\Attributes\Action; use Mantle\Support\Attributes\Filter; use Mantle\Support\Traits\Hookable; /** * Example Class */ class Example_Class { use Hookable; #[Action( 'example_action' )] public function example_action( $args ) { // Your action code here. } #[Filter( 'example_filter', priority: 40 )] public function example_filter( $args ) { // Your filter code here. } #[Action( 'pre_get_posts', priority: 20 )] public function pre_get_posts_at_20( $query ) { // Your action code here. } #[Filter( 'template_redirect', priority: 20 )] public function template_redirect_at_20( $template ) { // Your filter code here. } } ``` The `Action` and `Filter` attributes are used to define the hook name and priority. The priority defaults to 10 but can be changed by passing a second argument to the attribute. When using attribute actions/filters, the method name does not matter. You can name your methods whatever you like. #### Usage With Method Names[​](#usage-with-method-names "Direct link to Usage With Method Names") You can also use the method names to define the hook name and priority. The method name should be in the format `action__hook_name` or `filter__hook_name`. The priority can be defined by appending `_at_{priority}` to the method name. For example: ```php namespace App; use Mantle\Support\Traits\Hookable; /** * Example Class */ class Example_Class { use Hookable; public function __construct() { $this->register_hooks(); // Your constructor code here. } // This method is called on the 'example_action' action at 10 priority. public function action__example_action( $args ) { // Your action code here. } // This method is called on the 'example_filter' filter at 10 priority. public function filter__example_filter( $args ) { // Your filter code here. } // This method is called on 'pre_get_posts' action at 20 priority. public function action__pre_get_posts_at_20( $query ) { // Your action code here. } // This method is called on 'template_redirect` filter at 20 priority. public function filter__template_redirect_at_20( $template ) { // Your filter code here. } } ``` tip [Attributes](#usage-with-attributes) are the preferred way to define hooks in your classes. #### Using Your Own Constructor[​](#using-your-own-constructor "Direct link to Using Your Own Constructor") Out of the box, the `Hookable` trait will implement a constructor that will automatically call the `register_hooks` method. You can override this with your own constructor if you need to and call `register_hooks` manually in your own constructor: ```php namespace App; use Mantle\Support\Attributes\Action; use Mantle\Support\Attributes\Filter; use Mantle\Support\Traits\Hookable; /** * Example Class */ class Example_Class { use Hookable; public function __construct() { $this->register_hooks(); // Your constructor code here. } #[Action( 'example_action', 10 )] public function example_action( $args ) { // Your action code here. } #[Filter( 'example_filter', 10 )] public function example_filter( $args ) { // Your filter code here. } #[Action( 'pre_get_posts', 20 )] public function pre_get_posts_at_20( $query ) { // Your action code here. } #[Filter( 'template_redirect', 20 )] public function template_redirect_at_20( $template ) { // Your filter code here. } } ``` #### Conditional Validation[​](#conditional-validation "Direct link to Conditional Validation") If you want to conditionally register hooks based on some logic, you can do so by adding a validator attribute to your method. An attribute that implements [\`Mantle\Types\Validator](/docs/features/types.md) can be used to determine if the hook should be registered or not. ```php namespace App; use Mantle\Support\Attributes\Action; use Mantle\Support\Attributes\Filter; use Mantle\Support\Traits\Hookable; use Mantle\Types\Validator; #[\Attribute] class Example_Validator implements Validator { public function validate( mixed $value ): bool { // Your validation logic here. return true; // or false } } /** * Example Class */ class Example_Class { use Hookable; #[Action( 'example_action' )] #[Example_Validator] public function example_action( $args ) { // Your action code here is only added to the 'example_action' hook if the validator returns true. } } ``` You can also use any of the [available validators](/docs/features/types.md#available-validators) included with the types package. --- ### HTML Parsing, Assertions, and Manipulation The `HTML` class provides methods to query, manipulate, and assert against HTML strings and documents. It is built on top of the [`DomCrawler` component from Symfony](https://symfony.com/doc/current/components/dom_crawler.html), which provides a powerful and flexible way to work with HTML. #### Usage[​](#usage "Direct link to Usage") The HTML class can be instantiated with the `HTML` class or by using the `html()` helper function. ```php use Mantle\Support\HTML; use function Mantle\Support\Helpers\html_string; $html = new HTML( '
Hello World
' ); // Or using the helper function. $html = html_string( '
Hello World
' ); ``` The HTML class supports being passed a HTML string, document, `DOMDocument`, `DOMNode`, or `DOMNodeList`. It will parse the HTML using `DOMDocument`. ##### Filtering[​](#filtering "Direct link to Filtering") The HTML class provides methods to filter nodes based on various criteria, such as ID, class name, tag name, and custom selectors. ###### Query Selector[​](#query-selector "Direct link to Query Selector") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->get_by_selector( '.example' ); foreach ( $elements as $element ) { echo $element->text(); // Outputs: Hello World, Hello Universe } ``` You can also retrieve the first element that matches a specific selector using the `first_by_selector` method. ```php $element = html_string( $html )->first_by_selector( '.example' ); echo $element->text(); // Outputs: Hello World ``` ###### XPath[​](#xpath "Direct link to XPath") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; // Multiple elements. $elements = html_string( $html )->get_by_xpath( '//p[@class="example"]' ); // Single element. $element = html_string( $html )->first_by_xpath( '//p[@class="example"]' ); ``` ###### ID / Tag / Test ID[​](#id--tag--test-id "Direct link to ID / Tag / Test ID") You can also retrieve elements by their ID, tag name, or test ID using the `first_by_id`, `first_by_tag`, and `first_by_testid` methods respectively. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; // Retrieve by ID. $element = html_string( $html )->first_by_id( 'test' ); // Retrieve by tag name. $element = html_string( $html )->first_by_tag( 'p' ); // Retrieve by test ID. $element = html_string( $html )->first_by_testid( 'test' ); ``` There are also `get_by_*` versions of these methods that return all matching elements. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; // Retrieve all elements by tag name. $elements = html_string( $html )->get_by_tag( 'p' ); // Retrieve all elements by test ID. $elements = html_string( $html )->get_by_testid( 'test' ); ``` ##### Traversing and Looping[​](#traversing-and-looping "Direct link to Traversing and Looping") The HTML class is iterable, allowing you to loop through the nodes it contains. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->get_by_selector( '.example' ); foreach ( $elements as $element ) { // $element is a instanceof the `HTML` class. echo $element->text(); // Outputs: Hello World, Hello Universe } ``` Access node by its position on the list: ```php $html->filter( 'body > p' )->eq( 0 ); ``` Get the first or last node of the current selection: ```php $html->filter( 'body > p' )->first(); $html->filter( 'body > p' )->last(); ``` Get the nodes of the same level as the current selection: ```php $html->filter( 'body > p' )->siblings(); ``` Get the same level nodes after or before the current selection: ```php $html->filter( 'body > p' )->nextAll(); $html->filter( 'body > p' )->previousAll(); ``` Get all the child or ancestor nodes: ```php $html->filter( 'body' )->children(); $html->filter( 'body > p' )->ancestors(); ``` Get all the direct child nodes matching a CSS selector: ```php $html->filter( 'body' )->children( 'p.lorem' ); ``` Get the first parent (heading toward the document root) of the element that matches the provided selector: ```php $html->closest( 'p.lorem' ); ``` ##### Accessing Node Values[​](#accessing-node-values "Direct link to Accessing Node Values") You can access the text content of a node using the `tag_name()`, `text()`, or `innerText()` methods. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_selector( '.example' ); // Tag name. echo $element->tag_name(); // Outputs: 'p'. // Text content. echo $element->text(); // Outputs: 'Hello World'. ``` You can also retrieve the attributes of a node using the `get_attribute()` method. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; $html = html_string( $html ); // Get attribute value. $html->first_by_id( 'test' )->get_attribute( 'class' ); // Outputs: 'example'. $html->first_by_selector( '.example' )->get_attribute( 'data-example' ); // Outputs: '1234'. $html->first_by_selector( '.example' )->get_data( 'example' ); // Outputs: '1234'. ``` ##### Modifying Node Values[​](#modifying-node-values "Direct link to Modifying Node Values") You can modify the attributes, classes, and content of nodes using the `modify()` method of the HTML class as well as other methods to mutate the element's contents, attributes, etc. ```php use Mantle\Support\HTML; use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $html = html_string( $html ); $html->filter( 'p' )->modify( function ( HTML $node ) { // Add a class to all

elements. $node->add_class( 'modified' ); } ); // You can also replace the entire contents of a node. $html->filter( 'p' )->modify( fn ( HTML $node ) => "New content" ); ``` #### Methods[​](#methods "Direct link to Methods") * [Retrieving Nodes](#retrieving-nodes) * [`count`](#count) * [`filter/get_by_selector`](#filterget_by_selector) * [`first_by_id`](#first_by_id) * [`first_by_selector`](#first_by_selector) * [`first_by_tag`](#first_by_tag) * [`first_by_testid`](#first_by_testid) * [`first_by_xpath`](#first_by_xpath) * [`get_by_tag`](#get_by_tag) * [`get_by_testid`](#get_by_testid) * [`get_by_xpath`](#get_by_xpath) * [Modifying Nodes](#modifying-nodes) * [`add_class`](#add_class) * [`after`](#after) * [`append`](#append) * [`before`](#before) * [`empty`](#empty) * [`get_data`](#get_data) * [`has_any_class`](#has_any_class) * [`has_class`](#has_class) * [`modify`](#modify) * [`prepend`](#prepend) * [`remove`](#remove) * [`remove_attribute`](#remove_attribute) * [`remove_class`](#remove_class) * [`remove_data`](#remove_data) * [`set_attribute`](#set_attribute) * [`set_data`](#set_data) * [`wrap`](#wrap) * [`wrap_all`](#wrap_all) * [`wrap_inner`](#wrap_inner) * [Iteration](#iteration) * [`next_until`](#next_until) * [`previous_until`](#previous_until) * [Node Information](#node-information) * [`dd`](#dd) * [`dump`](#dump) * [`has_nodes`](#has_nodes) * [`tag_name`](#tag_name) * [`text`](#text) * [Assertions](#assertions) * [`assertHasChildren`](#asserthaschildren) * [`assertHasNodes`](#asserthasnodes) * [`assertNodeHasAnyClass`](#assertnodehasanyclass) * [`assertNodeHasClass`](#assertnodehasclass) * [assertQuerySelectorExists](#assertqueryselectorexists) * [assertQuerySelectorMissing](#assertqueryselectormissing) * [assertElementExists](#assertelementexists) * [assertElementMissing](#assertelementmissing) * [assertElementExistsByClass](#assertelementexistsbyclass) * [assertElementMissingByClass](#assertelementmissingbyclass) * [assertElementExistsById](#assertelementexistsbyid) * [assertElementMissingById](#assertelementmissingbyid) * [assertElementExistsByTagName](#assertelementexistsbytagname) * [assertElementMissingByTagName](#assertelementmissingbytagname) * [assertElementCount](#assertelementcount) * [assertQuerySelectorCount](#assertqueryselectorcount) * [assertElementExistsByTestId](#assertelementexistsbytestid) * [assertElementMissingByTestId](#assertelementmissingbytestid) * [assertElement](#assertelement) * [assertQuerySelector](#assertqueryselector) ##### Retrieving Nodes[​](#retrieving-nodes "Direct link to Retrieving Nodes") ###### `count`[​](#count "Direct link to count") Returns the number of nodes in the current selection. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->filter( '.example' ); $elements->count(); // Outputs: 2 ``` ###### `filter/get_by_selector`[​](#filterget_by_selector "Direct link to filterget_by_selector") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->filter( '.example' ); $elements = html_string( $html )->get_by_selector( '.example' ); ``` ###### `first_by_id`[​](#first_by_id "Direct link to first_by_id") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_id( 'test' ); ``` ###### `first_by_selector`[​](#first_by_selector "Direct link to first_by_selector") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_selector( '.example' ); ``` ###### `first_by_tag`[​](#first_by_tag "Direct link to first_by_tag") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_tag( 'p' ); ``` ###### `first_by_testid`[​](#first_by_testid "Direct link to first_by_testid") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_testid( 'example' ); ``` ###### `first_by_xpath`[​](#first_by_xpath "Direct link to first_by_xpath") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $element = html_string( $html )->first_by_xpath( '//p[@class="example"]' ); ``` ###### `get_by_tag`[​](#get_by_tag "Direct link to get_by_tag") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->get_by_tag( 'p' ); ``` ###### `get_by_testid`[​](#get_by_testid "Direct link to get_by_testid") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->get_by_testid( 'example' ); ``` ###### `get_by_xpath`[​](#get_by_xpath "Direct link to get_by_xpath") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $elements = html_string( $html )->get_by_xpath( '//p[@class="example"]' ); ``` ##### Modifying Nodes[​](#modifying-nodes "Direct link to Modifying Nodes") ###### `add_class`[​](#add_class "Direct link to add_class") Adds a class to the element. Supports multiple classes. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->add_class( 'new-class' ); html_string( $html )->first_by_selector( '.example' )->add_class( 'new-class', 'another-class' ); ``` ###### `after`[​](#after "Direct link to after") Inserts content after the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->after( 'After' ); /* Outputs:

Hello World

After

Hello Universe

*/ ``` ###### `append`[​](#append "Direct link to append") Appends content to the end of the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->append( 'Appended' ); /* Outputs:

Hello WorldAppended

Hello Universe

*/ ``` ###### `before`[​](#before "Direct link to before") Inserts content before the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->before( '

Before

' ); /* Outputs:

Before

Hello World

Hello Universe

*/ ``` ###### `empty`[​](#empty "Direct link to empty") Empties the content of the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->empty(); /* Outputs:

Hello Universe

*/ ``` ###### `get_data`[​](#get_data "Direct link to get_data") ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->get_data( 'example' ); // Outputs: '1234'. ``` ###### `has_any_class`[​](#has_any_class "Direct link to has_any_class") Checks if the element has any of the specified classes. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->has_any_class( 'example', 'another-class' ); // Outputs: true ``` ###### `has_class`[​](#has_class "Direct link to has_class") Checks if the element has all of the specified classes. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->has_class( 'example' ); // Outputs: true html_string( $html )->first_by_selector( '.example' )->has_class( 'example', 'another-class' ); // Outputs: false ``` ###### `modify`[​](#modify "Direct link to modify") Modifies the element using a callback function. The callback receives the current crawler as an argument. You can modify the element and return null/void or you can return a new element to replace the current one. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; $html = html_string( $html ); $html->filter( '.example' )->first()->modify( function ( HTML $node ) { // Add a class to the element. $node->add_class( 'modified' ); } ); $html->filter( '.example' )->last()->modify( function ( HTML $node ) { // Replace the content of the element. return 'New content'; } ); ``` ###### `prepend`[​](#prepend "Direct link to prepend") Prepends content to the beginning of the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->prepend( 'Prepended' ); /* Outputs:

PrependedHello World

Hello Universe

*/ ``` ###### `remove`[​](#remove "Direct link to remove") Removes the element from the DOM. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->remove(); /* Outputs:

Hello Universe

*/ ``` ###### `remove_attribute`[​](#remove_attribute "Direct link to remove_attribute") Removes an attribute from the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_selector( '.example' )->remove_attribute( 'class' ); /* Outputs:

Hello World

Hello Universe

*/ ``` ###### `remove_class`[​](#remove_class "Direct link to remove_class") Removes a class from the element. Supports multiple classes. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->remove_class( 'example' ); /* Outputs:

Hello World

*/ ``` ###### `remove_data`[​](#remove_data "Direct link to remove_data") Removes a data attribute from the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->remove_data( 'example' ); /* Outputs:

Hello World

*/ ``` ###### `set_attribute`[​](#set_attribute "Direct link to set_attribute") Sets an attribute on the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->set_attribute( 'data-example', '1234' ); /* Outputs:

Hello World

*/ ``` ###### `set_data`[​](#set_data "Direct link to set_data") Sets a data attribute on the element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_selector( '.example' )->set_data( 'example', '1234' ); /* Outputs:

Hello World

*/ ``` ###### `wrap`[​](#wrap "Direct link to wrap") Wraps the element with the specified HTML or element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'
  • Hello World
  • Hello Universe
HTML; html_string( $html )->filter( 'ul' )->wrap( '
' ); /* Outputs:
  • Hello World
  • Hello Universe
*/ ``` ###### `wrap_all`[​](#wrap_all "Direct link to wrap_all") Wraps all matched elements with the specified HTML or element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->filter( 'p' )->wrap_all( '
' ); /* Outputs:

Hello World

Hello Universe

*/ ``` ###### `wrap_inner`[​](#wrap_inner "Direct link to wrap_inner") Wraps the inner content of the element with the specified HTML or element. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'
  • Hello World
  • Hello Universe
HTML; html_string( $html )->filter( 'li' )->wrap_inner( '' ); /* Outputs:
  • Hello World
  • Hello Universe
*/ ``` ##### Iteration[​](#iteration "Direct link to Iteration") ###### `next_until`[​](#next_until "Direct link to next_until") Returns all nodes after a condition is met. Only includes the last node that matched the condition if `$include` is set to `true`. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

Hello Galaxy

HTML; // Contains the "Hello Galaxy" node. $elements = html_string( $html )->filter( 'p' )->next_until( fn ( HTML $element ) => $element->text() === 'Hello Universe', ); // Contains the "Hello Universe" and "Hello Galaxy" nodes. $elements = html_string( $html )->filter( 'p' )->next_until( fn ( HTML $element ) => $element->text() === 'Hello Universe', include: true ); ``` ###### `previous_until`[​](#previous_until "Direct link to previous_until") Returns all nodes before a condition is met. Only includes the last node that matched the condition if `$include` is set to `true`. ```php use function Mantle\Support\Helpers\html_string; $html = <<<'HTML'

Hello World

Hello Universe

Hello Galaxy

HTML; // Contains the "Hello World" node. $elements = html_string( $html )->filter( 'p' )->previous_until( fn ( HTML $element ) => $element->text() === 'Hello Universe', ); // Contains the "Hello World" and "Hello Universe" nodes. $elements = html_string( $html )->filter( 'p' )->previous_until( fn ( HTML $element ) => $element->text() === 'Hello Universe', include: true ); ``` ##### Node Information[​](#node-information "Direct link to Node Information") ###### `dd`[​](#dd "Direct link to dd") Dumps the current nodes and exits the script. ###### `dump`[​](#dump "Direct link to dump") Dumps the current nodes for debugging purposes and returns static. ###### `has_nodes`[​](#has_nodes "Direct link to has_nodes") Checks if the current selection has any nodes. ###### `tag_name`[​](#tag_name "Direct link to tag_name") Returns the tag name of the first node in the current selection. ###### `text`[​](#text "Direct link to text") Returns the text content of the first node in the current selection. ##### Assertions[​](#assertions "Direct link to Assertions") The assertions available on the `HTML` class extend the same assertions used on the [Element Assertions](/docs/testing/requests.md#element-assertions) used when testing HTTP requests. ###### `assertHasChildren`[​](#asserthaschildren "Direct link to asserthaschildren") Assert that the current node has children. ```php use function Mantle\Support\Helpers\html_string; class ExampleTest extends TestCase { public function testExample() { $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->first_by_id( 'test' )->assertHasChildren(); html_string( $html )->first_by_id( 'test' )->assertHasChildren( selector: 'p' ); html_string( $html )->first_by_id( 'test' )->assertHasChildren( selector: 'p', count: 2 ); } } ``` ###### `assertHasNodes`[​](#asserthasnodes "Direct link to asserthasnodes") Assert that the current selection has nodes. ```php use function Mantle\Support\Helpers\html_string; class ExampleTest extends TestCase { public function testExample() { $html = <<<'HTML'

Hello World

Hello Universe

HTML; html_string( $html )->filter( 'p' )->assertHasNodes(); html_string( $html )->filter( 'p' )->assertHasNodes( count: 2 ); } } ``` ###### `assertNodeHasAnyClass`[​](#assertnodehasanyclass "Direct link to assertnodehasanyclass") Assert that the current node has any of the specified classes. ```php use function Mantle\Support\Helpers\html_string; class ExampleTest extends TestCase { public function testExample() { $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_id( 'test' )->assertNodeHasAnyClass( 'example', 'another-class' ); } } ``` ###### `assertNodeHasClass`[​](#assertnodehasclass "Direct link to assertnodehasclass") Assert that the current node has all of the specified classes. ```php use function Mantle\Support\Helpers\html_string; class ExampleTest extends TestCase { public function testExample() { $html = <<<'HTML'

Hello World

HTML; html_string( $html )->first_by_id( 'test' )->assertNodeHasClass( 'example' ); // true html_string( $html )->first_by_id( 'test' )->assertNodeHasClass( 'example', 'another-class' ); // true html_string( $html )->first_by_id( 'test' )->assertNodeHasClass( 'example', 'non-existent-class' ); // false } } ``` ###### assertQuerySelectorExists[​](#assertqueryselectorexists "Direct link to assertQuerySelectorExists") Assert that a given CSS selector exists in the HTML. ```php $html->assertQuerySelectorExists( string $selector ); ``` ###### assertQuerySelectorMissing[​](#assertqueryselectormissing "Direct link to assertQuerySelectorMissing") Assert that a given CSS selector does not exist in the HTML. ```php $html->assertQuerySelectorMissing( string $selector ); ``` ###### assertElementExists[​](#assertelementexists "Direct link to assertElementExists") Assert that a given XPath exists in the HTML. ```php $html->assertElementExists( string $expression ); ``` ###### assertElementMissing[​](#assertelementmissing "Direct link to assertElementMissing") Assert that a given XPath does not exist in the HTML. ```php $html->assertElementMissing( string $expression ); ``` ###### assertElementExistsByClass[​](#assertelementexistsbyclass "Direct link to assertElementExistsByClass") Assert that a given class exists in the HTML. ```php $html->assertElementExistsByClass( string $class ); ``` ###### assertElementMissingByClass[​](#assertelementmissingbyclass "Direct link to assertElementMissingByClass") Assert that a given class does not exist in the HTML. ```php $html->assertElementMissingByClass( string $class ); ``` ###### assertElementExistsById[​](#assertelementexistsbyid "Direct link to assertElementExistsById") Assert that a given ID exists in the HTML. ```php $html->assertElementExistsById( string $id ); ``` ###### assertElementMissingById[​](#assertelementmissingbyid "Direct link to assertElementMissingById") Assert that a given ID does not exist in the HTML. ```php $html->assertElementMissingById( string $id ); ``` ###### assertElementExistsByTagName[​](#assertelementexistsbytagname "Direct link to assertElementExistsByTagName") Assert that a given tag name exists in the HTML. ```php $html->assertElementExistsByTagName( string $tag_name ); ``` ###### assertElementMissingByTagName[​](#assertelementmissingbytagname "Direct link to assertElementMissingByTagName") Assert that a given tag name does not exist in the HTML. ```php $html->assertElementMissingByTagName( string $tag_name ); ``` ###### assertElementCount[​](#assertelementcount "Direct link to assertElementCount") Assert that the HTML has the expected number of elements matching the given XPath expression. ```php $html->assertElementCount( string $expression, int $expected ); ``` ###### assertQuerySelectorCount[​](#assertqueryselectorcount "Direct link to assertQuerySelectorCount") Assert that the HTML has the expected number of elements matching the given CSS selector. ```php $html->assertQuerySelectorCount( string $selector, int $expected ); ``` ###### assertElementExistsByTestId[​](#assertelementexistsbytestid "Direct link to assertElementExistsByTestId") Assert that an element with the given `data-testid` attribute exists in the HTML. ```php $html->assertElementExistsByTestId( string $test_id ); ``` ###### assertElementMissingByTestId[​](#assertelementmissingbytestid "Direct link to assertElementMissingByTestId") Assert that an element with the given `data-testid` attribute does not exist in the HTML. ```php $html->assertElementMissingByTestId( string $test_id ); ``` ###### assertElement[​](#assertelement "Direct link to assertElement") Assert that the given element exists in the HTML and passes the given assertion. This can be used to make custom assertions against the element that cannot be expressed in a simple XPath expression or query selector. ```php $html->assertElement( string $expression, callable $assertion, bool $pass_any = false ); ``` If `$pass_any` is `true`, the assertion will pass if any of the elements pass the assertion. Otherwise, all elements must pass the assertion. Let's take a look at an example: ```php use DOMElement; $html->assertElement( '//div', fn ( DOMElement $element ) => $this->assertEquals( 'Hello World', $element->textContent ) && $this->assertNotEmpty( $element->getAttribute( 'class' ) ) ); }, ); ``` ###### assertQuerySelector[​](#assertqueryselector "Direct link to assertQuerySelector") Assert that the given CSS selector exists in the HTML and passes the given assertion. Similar to `assertElement`, this can be used to make custom assertions against the element that cannot be expressed in a simple XPath expression or query selector. ```php $html->assertQuerySelector( string $selector, callable $assertion, bool $pass_any = false ); ``` Let's take a look at an example: ```php use DOMElement; $html->assertQuerySelector( 'div > p', fn ( DOMElement $element ) => $this->assertEquals( 'Hello World', $element->textContent ) && $this->assertNotEmpty( $element->getAttribute( 'class' ) ) ); }, ); ``` --- ### Macroable Macros allow you to add methods to classes from outside the class. This is useful when you want to add functionality to a class without modifying the class itself. Many classes within Mantle use this trait to allow for the addition of functionality to the class from outside the class. #### Usage[​](#usage "Direct link to Usage") To use the `Macroable` trait, you can use the following pattern: ```php namespace App; use Mantle\Support\Traits\Macroable; class Example_Class { use Macroable; public function __construct( public int $number = 1, ) {} public function double(): static { $this->number *= 2; return $this; } // Your class code here. } ``` Then you can register a macro like this: ```php Example_Class::macro('thirty', function() { // Return an instance of your class with your specific customizations. return new Example_Class( 30 ); }); ``` Then you can call the macro like this: ```php $instance = Example_Class::thirty(); ``` Macros can also be used to add functionality to already instantiated objects: ```php $instance = new Example_Class(); $instance->macro('triple', function() { $this->number *= 3; return $this; }); ``` Then you can call the macro like this: ```php $instance = new Example_Class(); // Double it with the class method. $instance->double(); // Call the macro. $instance->triple(); // The number should now be 6. echo $instance->number; // 6 ``` --- ### Type-safe Options, Object Metadata and Mixed Data When working with WordPress, you may need to retrieve and manipulate options or object meta data. Out of the box, the data will come back as `mixed`. You can and should check the type of the data before using it. This can be cumbersome and error-prone. Mantle includes a `option` helper function that can be used to retrieve an option's value in a type-safe manner. It can also update the option's value and manipulate it in various ways. Mantle also includes `post_meta`, `term_meta`, `user_meta`, and `comment_meta` helper functions that can be used to retrieve and manipulate object meta data in a type-safe manner. These build off the `mixed` helper that can be used to manage and manipulate mixed data from any source. #### Usage[​](#usage "Direct link to Usage") ##### Usage for Options[​](#usage-for-options "Direct link to Usage for Options") The `option` function can be used to retrieve an option's value and does not require the rest of the framework to do so: ```php use function Mantle\Support\Helpers\option; // Set the value of an option. update_option( 'option_name', 'option_value' ); // Get the value of an option as a string. option( 'option_name' )->string(); // Get the value of an option as an integer. option( 'option_name' )->integer(); // Get the value of an option as a boolean. option( 'option_name' )->boolean(); // Get the value of an option as an array. option( 'option_name' )->array(); ``` ##### Usage for Object Metadata[​](#usage-for-object-metadata "Direct link to Usage for Object Metadata") The `post_meta`, `term_meta`, `user_meta`, and `comment_meta` functions can be used to retrieve object meta data and do not require the rest of the framework to do so: ```php use function Mantle\Support\Helpers\post_meta; use function Mantle\Support\Helpers\term_meta; use function Mantle\Support\Helpers\user_meta; use function Mantle\Support\Helpers\comment_meta; // Retrieve post meta data. post_meta( 1234, 'meta_key' )->string(); // string post_meta( 1234, 'meta_key' )->boolean(); // bool post_meta( 1234, 'meta_key' )->integer(); // int // Retrieve term meta data. term_meta( 1234, 'meta_key' )->string(); // string term_meta( 1234, 'meta_key' )->boolean(); // bool ``` Meta data can also be updated and deleted: ```php use function Mantle\Support\Helpers\post_meta; // Update post meta data. $meta = post_meta( 1234, 'meta_key' ); if ( $meta->is_empty() ) { $meta->set( 'meta_value' ); $meta->save(); } // Delete post meta data. post_meta( 1234, 'meta_key' )->delete(); ``` ##### Usage for Query Variables[​](#usage-for-query-variables "Direct link to Usage for Query Variables") The `mixed_query_var` helper can be used to retrieve query variables in a type-safe manner: ```php use function Mantle\Support\Helpers\mixed_query_var; // Get the value of a query variable as a string. mixed_query_var( 'query_var_name' )->string(); ``` ##### Usage for Mixed Data[​](#usage-for-mixed-data "Direct link to Usage for Mixed Data") The `mixed` helper can be used to manage and manipulate mixed data from any source. You can pass any value to the `mixed` helper and it will return a `Mantle\Support\Mixed_Data` object that provides a consistent interface for retrieving and manipulating the data. This is useful when you need to work with data that may come from various sources, such as user input, API responses, or database queries. ```php use function Mantle\Support\Helpers\mixed; $value = mixed( 'some_value' )->string(); // string $value = mixed( '1234' )->integer(); // int ``` You can also use the `Mixed_Data::of()` method to create a new instance of the `Mixed_Data` class with a specific value: ```php use Mantle\Support\Mixed_Data; $value = Mixed_Data::of( 'some_value' )->string(); // string ``` #### Methods[​](#methods "Direct link to Methods") The following methods are shared between the `option` helper and all the object meta data helpers. * [`array()`](#array) * [`boolean()`](#boolean) * [`collect()`](#collect) * [`date()`](#date) * [`dd()`](#dd) * [`dump()`](#dump) * [`float()`](#float) * [`int()`](#int) * [`is_empty()`](#is_empty) * [`is_not_null()`](#is_not_null) * [`is_type( string $type )`](#is_type-string-type-) * [`is_not_type()`](#is_not_type) * [`is_array()`](#is_array) * [`is_not_array()`](#is_not_array) * [`is_object()`](#is_object) * [`is_not_object()`](#is_not_object) * [`object()`](#object) * [`string()`](#string) * [`stringable()`](#stringable) * [`throw()`](#throw) * [`throw_if()`](#throw_if) * [`value()`](#value) * [Update and Delete Methods](#update-and-delete-methods) * [`set()`](#set) * [`delete()`](#delete) ##### `array()`[​](#array "Direct link to array") Get the value of the option as an array. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->array(); // array ``` You can use the `get()` method to retrieve a specific key from the array (supports dot notation): ```php use function Mantle\Support\Helpers\option; update_option( 'example_option', [ 'key' => 'value', 'nested' => [ 'key' => 'value', ], ] ); $value = option( 'example_option' )->array(); // array{key: 'value', nested: array{key: 'value'}} $nested = option( 'example_option' )->array( 'nested' ); // array{key: 'value'} $sub = option( 'example_option' )->get( 'key' ); // string: value // Supports dot notation. $sub = option( 'example_option' )->get( 'nested.key' ); // string: value // Check if a key exists. $has = option( 'example_option' )->has( 'key' ); // bool: true ``` ##### `boolean()`[​](#boolean "Direct link to boolean") Get the value of the option as a boolean. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->boolean(); // bool ``` ##### `collect()`[​](#collect "Direct link to collect") Get the value of the option as a [collection](/docs/features/support/collections.md). ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->collect(); // Mantle\Support\Collection ``` ##### `date()`[​](#date "Direct link to date") Get the value of the option as a Carbon date instance. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->date(); // Carbon\Carbon ``` It also supports passing a format and/or timezone: ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->date( format: 'Y-m-d H:i:s', timezone: 'UTC' ); // Carbon\Carbon ``` ##### `dd()`[​](#dd "Direct link to dd") Dump the value of the option and end the script execution. ```php use function Mantle\Support\Helpers\option; option( 'option_name' )->dd(); ``` ##### `dump()`[​](#dump "Direct link to dump") Dump the value of the option and continue script execution. ```php use function Mantle\Support\Helpers\option; option( 'option_name' )->dump(); ``` ##### `float()`[​](#float "Direct link to float") Get the value of the option as a float. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->float(); // float ``` ##### `int()`[​](#int "Direct link to int") Get the value of the option as an integer. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->int(); // int // or $value = option( 'option_name' )->integer(); // int ``` ##### `is_empty()`[​](#is_empty "Direct link to is_empty") Check if the value of the option is empty. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_empty() ) { // The option is empty. } ``` ##### `is_not_null()`[​](#is_not_null "Direct link to is_not_null") Check if the value of the option is not `null`. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_not_null() ) { // The option is not null. } ``` ##### `is_type( string $type )`[​](#is_type-string-type- "Direct link to is_type-string-type-") Check if the value of the option is of a specific type. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_type( 'string' ) ) { // The option is a string. } ``` ##### `is_not_type()`[​](#is_not_type "Direct link to is_not_type") Check if the value of the option is not of a specific type. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_not_type( 'string' ) ) { // The option is not a string. } ``` ##### `is_array()`[​](#is_array "Direct link to is_array") Check if the value of the option is an array. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_array() ) { // The option is an array. } ``` ##### `is_not_array()`[​](#is_not_array "Direct link to is_not_array") Check if the value of the option is not an array. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_not_array() ) { // The option is not an array. } ``` ##### `is_object()`[​](#is_object "Direct link to is_object") Check if the value of the option is an object. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_object() ) { // The option is an object. } ``` ##### `is_not_object()`[​](#is_not_object "Direct link to is_not_object") Check if the value of the option is not an object. ```php use function Mantle\Support\Helpers\option; if ( option( 'option_name' )->is_not_object() ) { // The option is not an object. } ``` ##### `object()`[​](#object "Direct link to object") Get the value of the option as an object. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->object(); // object ``` You can use the `get()` method to retrieve a specific property from the object: ```php use function Mantle\Support\Helpers\option; update_option( 'example_option', (object) [ 'key' => 'value', 'nested' => (object) [ 'key' => 'value', ], ] ); $value = option( 'example_option' )->object(); // object{key: 'value', nested: object{key: 'value'}} $nested = option( 'example_option' )->object( 'nested' ); // object{key: 'value'} $sub = option( 'example_option' )->get( 'key' ); // string: value // Supports dot notation. $sub = option( 'example_option' )->get( 'nested.key' ); // string: value // Check if a property exists. $has = option( 'example_option' )->has( 'key' ); // bool: true ``` ##### `string()`[​](#string "Direct link to string") Get the value of the option as a string. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->string(); // string ``` ##### `stringable()`[​](#stringable "Direct link to stringable") Get the value of the option as a [Stringable](/docs/features/support/stringable.md) object. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->stringable(); // Mantle\Support\Stringable ``` ##### `throw()`[​](#throw "Direct link to throw") Throw an exception if the option is not able to be cast to the specified type. For example, if you try to cast an array to a string, an exception will be thrown. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->throw()->string(); // string // The following will throw an exception: $value = option( 'option_name' )->throw()->array(); ``` ##### `throw_if()`[​](#throw_if "Direct link to throw_if") Throw an exception if the option is not able to be cast to the specified type based on a condition. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' ) ->throw_if( 'production' !== wp_get_environment_type() ) ->string(); // string ``` ##### `value()`[​](#value "Direct link to value") Get the value of the option. Will return with a type of `mixed`. ```php use function Mantle\Support\Helpers\option; $value = option( 'option_name' )->value(); // mixed ``` ##### Update and Delete Methods[​](#update-and-delete-methods "Direct link to Update and Delete Methods") These methods can be used on object metadata and options. ###### `set()`[​](#set "Direct link to set") The `option` helper can also be used to update an option's or a object's meta data value: ```php use function Mantle\Support\Helpers\option; // Update the value of an option. $meta = option( 'option_name' ); $meta->set( 'new_value' ); $meta->save(); ``` ###### `delete()`[​](#delete "Direct link to delete") The `option` helper can also be used to delete an option or a object's meta data: ```php use function Mantle\Support\Helpers\option; // Delete an option. option( 'option_name' )->delete(); ``` --- ### Pipeline Mantle includes a pipeline class that uses the pipeline pattern to process a series of tasks in a specific order. This can be useful for processing data through a series of filters or middleware. The pipeline class powers the application's middleware for routing, databases, etc. #### Usage[​](#usage "Direct link to Usage") To use the pipeline class, you can use the following pattern: ```php use Closure; use Mantle\Support\Pipeline; $result = ( new Pipeline() ) ->send( $data ) ->through( [ // Middleware as a closure. function ( mixed $data, Closure $next ) { // Your task code here. return $next( $data ); }, // Modify the result of the pipeline. function ( mixed $data, Closure $next ) { $result = $next( $data ); // Modify the result of the pipeline. return $result; }, // Middleware can also be passed as a class name. ExampleMiddleware::class, ] ) ->then( function ( mixed $data ) { // Your final task code here. return $data; } ); ``` When the pipeline is executed, the data will be passed through each middleware in the order they are defined. The `send` method is used to pass the initial data to the pipeline. The `through` method is used to define the middleware to be executed. The `then` method is used to define the final task to be executed after all middleware have been processed. Each middleware "pipe" can modify the initial data that is passed on to the next pipe as well as modify the data that is returned from the pipeline. --- ### Singleton A singleton is a class that can only be instantiated once. It is often used to manage global state in a program. This can be useful for allowing a single global instance of a class to be shared across the entire program. note If you are working with the Mantle Framework you should use the [Container's `singleton` method](/docs/architecture.md) to create a singleton instance of a class. #### Usage[​](#usage "Direct link to Usage") To create a singleton class, you can use the following pattern: ```php namespace App; use Mantle\Support\Traits\Singleton; class Example_Class { use Singleton; protected function __construct() { // Your constructor code here } } ``` Then you can instantiate the class like this: ```php $instance = Example_Class::instance(); ``` The `instance` method will always return the same instance of the class. --- ### Stringable The Stringable class exists in the `Mantle\Support` namespace and is used to create a stringable object from a string or a stringable object. For more information see the [Laravel docs](https://laravel.com/docs/master/strings#method-str). #### Usage[​](#usage "Direct link to Usage") ```php use function Mantle\Support\Helpers\stringable; $string = stringable( 'String' ); // String $string->append( 'append' )->lower(); // stringappend ``` #### Available Methods[​](#available-methods "Direct link to Available Methods") * [`after`](#after) * [`after_last`](#after_last) * [`append`](#append) * [`newLine`](#newline) * [`ascii`](#ascii) * [`basename`](#basename) * [`char_at`](#char_at) * [`class_basename`](#class_basename) * [`before`](#before) * [`before_last`](#before_last) * [`between`](#between) * [`camel`](#camel) * [`contains`](#contains) * [`contains_all`](#contains_all) * [`dirname`](#dirname) * [`ends_with`](#ends_with) * [`exactly`](#exactly) * [`excerpt`](#excerpt) * [`explode`](#explode) * [`finish`](#finish) * [`trailingSlash`](#trailingslash) * [`untrailingSlash`](#untrailingslash) * [`untrailing`](#untrailing) * [`is`](#is) * [`is_ascii`](#is_ascii) * [`is_json`](#is_json) * [`is_uuid`](#is_uuid) * [`is_empty`](#is_empty) * [`is_not_empty`](#is_not_empty) * [`kebab`](#kebab) * [`length`](#length) * [`limit`](#limit) * [`lower`](#lower) * [`markdown`](#markdown) * [`inline_markdown`](#inline_markdown) * [`mask`](#mask) * [`match`](#match) * [`is_match`](#is_match) * [`match_all`](#match_all) * [`test`](#test) * [`pad_both`](#pad_both) * [`pad_left`](#pad_left) * [`pad_right`](#pad_right) * [`pipe`](#pipe) * [`plural`](#plural) * [`plural_studly`](#plural_studly) * [`prepend`](#prepend) * [`remove`](#remove) * [`reverse`](#reverse) * [`repeat`](#repeat) * [`replace`](#replace) * [`replace_array`](#replace_array) * [`replace_first`](#replace_first) * [`replace_last`](#replace_last) * [`replace_matches`](#replace_matches) * [`squish`](#squish) * [`start`](#start) * [`strip_tags`](#strip_tags) * [`upper`](#upper) * [`title`](#title) * [`headline`](#headline) * [`singular`](#singular) * [`slug`](#slug) * [`snake`](#snake) * [`startsWith`](#startswith) * [`studly`](#studly) * [`studlyUnderscore`](#studlyunderscore) * [`substr`](#substr) * [`substr_count`](#substr_count) * [`swap`](#swap) * [`trim`](#trim) * [`ltrim`](#ltrim) * [`rtrim`](#rtrim) * [`lcfirst`](#lcfirst) * [`ucfirst`](#ucfirst) * [`when_contains`](#when_contains) * [`when_contains_all`](#when_contains_all) * [`when_empty`](#when_empty) * [`when_not_empty`](#when_not_empty) * [`when_ends_with`](#when_ends_with) * [`when_exactly`](#when_exactly) * [`when_not_exactly`](#when_not_exactly) * [`when_is`](#when_is) * [`when_is_ascii`](#when_is_ascii) * [`when_is_uuid`](#when_is_uuid) * [`when_starts_with`](#when_starts_with) * [`when_test`](#when_test) * [`words`](#words) * [`word_count`](#word_count) * [`wrap`](#wrap) * [`dump`](#dump) * [`dd`](#dd) * [`value`](#value) * [`to_integer`](#to_integer) * [`to_float`](#to_float) * [`to_boolean`](#to_boolean) * [`to_date`](#to_date) ##### `after`[​](#after "Direct link to after") Return the remainder of a string after the first occurrence of a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example here' )->after( 'example ' ); // here ``` ##### `after_last`[​](#after_last "Direct link to after_last") Return the remainder of a string after the last occurrence of a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example again example here' )->after_last( 'example ' ); // here ``` ##### `append`[​](#append "Direct link to append") Append a string or an array of strings to the end of the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->append( 'append' ); // exampleappend stringable( 'example' )->append( [ 'append1', 'append2' ] ); // exampleappend1append2 ``` ##### `newLine`[​](#newline "Direct link to newline") Return a string with a new line character. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->newLine(); // example\n stringable( 'example' )->newLine( 2 ); // example\n\n ``` ##### `ascii`[​](#ascii "Direct link to ascii") Transliterate a UTF-8 value to ASCII. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->ascii(); ``` ##### `basename`[​](#basename "Direct link to basename") Get the trailing name component of the path. ```php use function Mantle\Support\Helpers\stringable; stringable( 'Example/Path/To/File.txt' )->basename(); // File.txt ``` ##### `char_at`[​](#char_at "Direct link to char_at") Get the character at a given index. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->char_at( 0 ); // e ``` ##### `class_basename`[​](#class_basename "Direct link to class_basename") Get the class name from a fully qualified class name. ```php use function Mantle\Support\Helpers\stringable; stringable( 'Example\Namespace\ClassName' )->class_basename(); // ClassName ``` ##### `before`[​](#before "Direct link to before") Return the portion of a string before the first occurrence of a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->before( 'ple' ); // ex ``` ##### `before_last`[​](#before_last "Direct link to before_last") Return the portion of a string before the last occurrence of a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->before_last( 'ple' ); // exam ``` ##### `between`[​](#between "Direct link to between") Return the portion of a string between two given values. ```php use function Mantle\Support\Helpers\stringable; stringable( 'abcdefgh' )->between( 'a', 'g' ); // bcdef ``` ##### `camel`[​](#camel "Direct link to camel") Convert a string to camel case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'Example Name Here' )->camel(); // exampleNameHere ``` ##### `contains`[​](#contains "Direct link to contains") Check if a string contains a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->contains( 'example' ); // true stringable( 'example' )->contains( 'Example' ); // true stringable( 'example' )->contains( 'Example', ignore_case: true ); // true ``` ##### `contains_all`[​](#contains_all "Direct link to contains_all") Check if a string contains all of the given values. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->contains_all( [ 'example', 'here' ] ); // false stringable( 'example here' )->contains_all( [ 'example', 'here' ] ); // true ``` ##### `dirname`[​](#dirname "Direct link to dirname") Get the directory name of a path. ```php use function Mantle\Support\Helpers\stringable; stringable( '/example/path/to/file.txt' )->dirname(); // /example/path/to ``` ##### `ends_with`[​](#ends_with "Direct link to ends_with") Check if a string ends with a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example here' )->ends_with( 'here' ); // true ``` ##### `exactly`[​](#exactly "Direct link to exactly") Check if a string is exactly equal to a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->exactly( 'example' ); // true ``` ##### `excerpt`[​](#excerpt "Direct link to excerpt") Get a string excerpt from a given string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'This is my example' )->excerpt( 'my', [ 'radius' => 3 ] ); // '...is my ex...' ``` ##### `explode`[​](#explode "Direct link to explode") Explode a string into an array. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example here|example' )->explode( '|' ); // [ 'example here', 'example' ] ``` ##### `finish`[​](#finish "Direct link to finish") Return the string with a given value appended to the end. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->finish( 'example' ); // exampleexample ``` ##### `trailingSlash`[​](#trailingslash "Direct link to trailingslash") Return the string with a trailing slash. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->trailingSlash(); // example/ ``` ##### `untrailingSlash`[​](#untrailingslash "Direct link to untrailingslash") Return the string without a trailing slash. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example/' )->untrailingSlash(); // example ``` ##### `untrailing`[​](#untrailing "Direct link to untrailing") Return the string without a given trailing value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example trim' )->untrailing( ' trim' ); // example ``` ##### `is`[​](#is "Direct link to is") Check if a string matches a given pattern. Supports \* wildcards. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is( 'example' ); // true stringable( 'example' )->is( 'ex*' ); // true stringable( 'example' )->is( 'ex*ample' ); // true stringable( 'example' )->is( 'ex*ample*' ); // true ``` ##### `is_ascii`[​](#is_ascii "Direct link to is_ascii") Check if a string is ASCII. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_ascii(); // true ``` ##### `is_json`[​](#is_json "Direct link to is_json") Check if a string is JSON. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_json(); // false stringable( '{"example": "example"}' )->is_json(); // true ``` ##### `is_uuid`[​](#is_uuid "Direct link to is_uuid") Check if a string is a valid UUID. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_uuid(); // false stringable( '550e8400-e29b-41d4-a716-446655440000' )->is_uuid(); // true ``` ##### `is_empty`[​](#is_empty "Direct link to is_empty") Check if a string is empty. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_empty(): bool; // false stringable( '' )->is_empty(): bool; // true ``` ##### `is_not_empty`[​](#is_not_empty "Direct link to is_not_empty") Check if a string is not empty. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_not_empty(): bool; // true stringable( '' )->is_not_empty(): bool; // false ``` ##### `kebab`[​](#kebab "Direct link to kebab") Convert a string to kebab case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->kebab(); // example stringable( 'Example Name Here' )->kebab(); // example-name-here ``` ##### `length`[​](#length "Direct link to length") Get the length of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->length(): int; // 7 ``` ##### `limit`[​](#limit "Direct link to limit") Limit the string to a given number of characters. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->limit( 3 ); // ex... stringable( 'example' )->limit( 3, '... continue reading?' ); // ex... continue reading? ``` ##### `lower`[​](#lower "Direct link to lower") Convert a string to lowercase. ```php use function Mantle\Support\Helpers\stringable; stringable( 'Example' )->lower(); // example ``` ##### `markdown`[​](#markdown "Direct link to markdown") Convert GitHub flavored Markdown into HTML. ```php use function Mantle\Support\Helpers\stringable; stringable( "# Example\nText here" )->markdown(); //

Example

Text here

``` ##### `inline_markdown`[​](#inline_markdown "Direct link to inline_markdown") Convert inline Markdown into HTML. ```php use function Mantle\Support\Helpers\stringable; stringable( "# Example\nText here" )->inline_markdown(); //

Example

Text here

``` ##### `mask`[​](#mask "Direct link to mask") Masks a portion of a string with a repeated character. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example@example.com' )->mask( '*', 3 ); // exa**************** ``` ##### `match`[​](#match "Direct link to match") Get the string matching the given pattern. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->match( /exam/ ); // exam ``` ##### `is_match`[​](#is_match "Direct link to is_match") Check if a string matches a given pattern. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->is_match( /exam/ ); // true ``` ##### `match_all`[​](#match_all "Direct link to match_all") Get the string matching the given pattern. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->match_all( /exam/ ); // [ 'exam' ] ``` ##### `test`[​](#test "Direct link to test") Check if a string matches a given pattern. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->test( /exam/ ); // true ``` ##### `pad_both`[​](#pad_both "Direct link to pad_both") Pad a string to the left and right to a given length. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->pad_both( '_', 3 ); // ___example___ ``` ##### `pad_left`[​](#pad_left "Direct link to pad_left") Pad a string to the left to a given length. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->pad_left( '_', 3 ); // ___example ``` ##### `pad_right`[​](#pad_right "Direct link to pad_right") Pad a string to the right to a given length. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->pad_right( 3, '_' ); // example___ ``` ##### `pipe`[​](#pipe "Direct link to pipe") Pipe the string to a given callback and return the result as a stringable object. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->pipe( 'md5' ); // md5'd value in Stringable object. ``` ##### `plural`[​](#plural "Direct link to plural") Get the plural form of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->plural(); // examples stringable( 'example' )->plural( 2 ); // examples stringable( 'example' )->plural( 1 ); // example ``` ##### `plural_studly`[​](#plural_studly "Direct link to plural_studly") Pluralize the last word of an English, studly caps case string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->plural_studly(); // Examples stringable( 'example' )->plural_studly( 2 ); // Examples stringable( 'example' )->plural_studly( 1 ); // Example ``` ##### `prepend`[​](#prepend "Direct link to prepend") Prepend a string or an array of strings to the beginning of the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->prepend( 'prepend' ); // prependexample ``` ##### `remove`[​](#remove "Direct link to remove") Remove a string or an array of strings from the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->remove( 'exam' ); // ple ``` ##### `reverse`[​](#reverse "Direct link to reverse") Reverse the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->reverse(); // elpmaxe ``` ##### `repeat`[​](#repeat "Direct link to repeat") Repeat the string a given number of times. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->repeat( 3 ); // exampleexampleexample ``` ##### `replace`[​](#replace "Direct link to replace") Replace all occurrences of a given value in the string with another value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->replace( 'example', 'replace' ); // replace ``` ##### `replace_array`[​](#replace_array "Direct link to replace_array") Replace a given value in the string sequentially with an array. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->replace_array( 'example', [ 'replace1', 'replace2' ] ); // replace1replace2 ``` ##### `replace_first`[​](#replace_first "Direct link to replace_first") Replace the first occurrence of a given value in the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->replace_first( 'example', 'replace' ); // replace ``` ##### `replace_last`[​](#replace_last "Direct link to replace_last") Replace the last occurrence of a given value in the string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->replace_last( 'example', 'replace' ); // replace ``` ##### `replace_matches`[​](#replace_matches "Direct link to replace_matches") Replace all occurrences of a given pattern in the string with a value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->replace_matches( /([a-z]+)/, 'replace' ); // replace ``` ##### `squish`[​](#squish "Direct link to squish") Remove all whitespace from the beginning and end of a string, and reduce multiple spaces to a single space. ```php use function Mantle\Support\Helpers\stringable; stringable( ' example ' )->squish(); // example ``` ##### `start`[​](#start "Direct link to start") Return the string with a given value prepended to the beginning. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->start( 'pre' ); // preexample ``` ##### `strip_tags`[​](#strip_tags "Direct link to strip_tags") Strip HTML tags from a string. ```php use function Mantle\Support\Helpers\stringable; stringable( '

example

' )->strip_tags(); // example ``` ##### `upper`[​](#upper "Direct link to upper") Convert a string to uppercase. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->upper(); // EXAMPLE ``` ##### `title`[​](#title "Direct link to title") Convert a string to title case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example string here' )->title(); // Example String Here ``` ##### `headline`[​](#headline "Direct link to headline") Convert a string to headline case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example string here' )->headline(); // Example String Here ``` ##### `singular`[​](#singular "Direct link to singular") Get the singular form of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'examples' )->singular(); // example ``` ##### `slug`[​](#slug "Direct link to slug") Generate a URL friendly "slug" from a given string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example word here' )->slug(); // example-word-here stringable( 'example word here' )->slug( '_' ); // example_word_here ``` ##### `snake`[​](#snake "Direct link to snake") Convert a string to snake case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example name here' )->snake(); // example_name_here ``` ##### `startsWith`[​](#startswith "Direct link to startswith") Check if a string starts with a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->startsWith( 'example' ); // true ``` ##### `studly`[​](#studly "Direct link to studly") Convert a string to studly caps case. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example name here' )->studly(); // ExampleNameHere ``` ##### `studlyUnderscore`[​](#studlyunderscore "Direct link to studlyunderscore") Convert a string to studly caps case with underscores. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example name here' )->studlyUnderscore(); // Example_Name_Here ``` ##### `substr`[​](#substr "Direct link to substr") Get a substring of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->substr( 2, 3 ); // amp ``` ##### `substr_count`[​](#substr_count "Direct link to substr_count") Count the number of occurrences of a substring in a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example here' )->substr_count( 'e' ); // 2 ``` ##### `swap`[​](#swap "Direct link to swap") Replace all occurrences of a given value in the string with another value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example words here', [ 'example' => 'some', 'here' => 'there' ] ); // some words there ``` ##### `trim`[​](#trim "Direct link to trim") Trim whitespace from the beginning and end of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( ' example ' )->trim(); // example ``` ##### `ltrim`[​](#ltrim "Direct link to ltrim") Trim whitespace from the beginning of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( ' example ' )->ltrim(); // 'example ' ``` ##### `rtrim`[​](#rtrim "Direct link to rtrim") Trim whitespace from the end of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( ' example ' )->rtrim(); // ' example' ``` ##### `lcfirst`[​](#lcfirst "Direct link to lcfirst") Convert the first character of a string to lowercase. ```php use function Mantle\Support\Helpers\stringable; stringable( 'Example' )->lcfirst(); // example ``` ##### `ucfirst`[​](#ucfirst "Direct link to ucfirst") Convert the first character of a string to uppercase. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->ucfirst(); // Example ``` ##### `when_contains`[​](#when_contains "Direct link to when_contains") Check if a string contains a given value and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_contains( string|iterable $needles, ?callable $callback, ?callable $default = null ); ``` ##### `when_contains_all`[​](#when_contains_all "Direct link to when_contains_all") Check if a string contains all of the given values and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_contains_all( array $needles, ?callable $callback, ?callable $default = null ); ``` ##### `when_empty`[​](#when_empty "Direct link to when_empty") Check if a string is empty and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_empty( ?callable $callback, ?callable $default = null ); ``` ##### `when_not_empty`[​](#when_not_empty "Direct link to when_not_empty") Check if a string is not empty and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_not_empty( ?callable $callback, ?callable $default = null ); ``` ##### `when_ends_with`[​](#when_ends_with "Direct link to when_ends_with") Check if a string ends with a given value and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_ends_with( string|iterable $needles, ?callable $callback, ?callable $default = null ); ``` ##### `when_exactly`[​](#when_exactly "Direct link to when_exactly") Check if a string is exactly equal to a given value and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_exactly( string $value, ?callable $callback, ?callable $default = null ); ``` ##### `when_not_exactly`[​](#when_not_exactly "Direct link to when_not_exactly") Check if a string is not exactly equal to a given value and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_not_exactly( string $value, ?callable $callback, ?callable $default = null ); ``` ##### `when_is`[​](#when_is "Direct link to when_is") Check if a string matches a given pattern and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_is( string|iterable $pattern, ?callable $callback, ?callable $default = null ); ``` ##### `when_is_ascii`[​](#when_is_ascii "Direct link to when_is_ascii") Check if a string is ASCII and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_is_ascii( ?callable $callback, ?callable $default = null ); ``` ##### `when_is_uuid`[​](#when_is_uuid "Direct link to when_is_uuid") Check if a string is a valid UUID and execute a callback if it is. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_is_uuid( ?callable $callback, ?callable $default = null ); ``` ##### `when_starts_with`[​](#when_starts_with "Direct link to when_starts_with") Check if a string starts with a given value and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_starts_with( string|iterable $needles, ?callable $callback, ?callable $default = null ); ``` ##### `when_test`[​](#when_test "Direct link to when_test") Check if a string matches a given pattern and execute a callback if it does. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->when_test( string $pattern, ?callable $callback, ?callable $default = null ); ``` ##### `words`[​](#words "Direct link to words") Get the first `n` words of a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->words( int $words = 100, string $end = '...' ); ``` ##### `word_count`[​](#word_count "Direct link to word_count") Get the number of words in a string. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->word_count( string|null $characters = null ): int; ``` ##### `wrap`[​](#wrap "Direct link to wrap") Wrap a string with a given value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->wrap( string $before, string|null $after = null ); ``` ##### `dump`[​](#dump "Direct link to dump") Dump the string and return it as a stringable object. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->dump(); ``` ##### `dd`[​](#dd "Direct link to dd") Dump the string and terminate the script. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->dd(): never; ``` ##### `value`[​](#value "Direct link to value") Get the string value. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->value(): string; ``` ##### `to_integer`[​](#to_integer "Direct link to to_integer") Convert the string to an integer. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->to_integer(): int; ``` ##### `to_float`[​](#to_float "Direct link to to_float") Convert the string to a float. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->to_float(): float; ``` ##### `to_boolean`[​](#to_boolean "Direct link to to_boolean") Convert the string to a boolean. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->to_boolean(): bool; ``` ##### `to_date`[​](#to_date "Direct link to to_date") Convert the string to a Carbon date object. ```php use function Mantle\Support\Helpers\stringable; stringable( 'example' )->to_date( string|null $format = null, string|null $tz = null ): ?\Carbon\Carbon; ``` --- ### URI Manipulation #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides a fluent interface for working with URIs through the `Uri` class. This class allows you to create, parse, and manipulate various components of a URI including scheme, host, path, query parameters, and fragments. The `Uri` class is built on top of the League URI package and provides a more friendly interface for common URI operations. Whether you need to build complex URLs, modify query parameters, or generate redirect responses, the `Uri` class makes these operations straightforward and expressive. #### Creating URI Instances[​](#creating-uri-instances "Direct link to Creating URI Instances") ##### Using the Helper Function[​](#using-the-helper-function "Direct link to Using the Helper Function") The simplest way to create a URI instance is using the `uri()` helper function: ```php use function Mantle\Support\Helpers\uri; // Create a URI from a string $uri = uri( 'https://example.com/path?query=value' ); // Get the current request URI $current = uri(); ``` ##### Using the Uri Class Directly[​](#using-the-uri-class-directly "Direct link to Using the Uri Class Directly") You can also use the `Uri` class directly to create instances: ```php use Mantle\Support\Uri; // Create a URI from a string $uri = new Uri( 'https://example.com/path?query=value' ); // Alternative static constructor $uri = Uri::of( 'https://example.com/path?query=value' ); // Get the current request URI $current = Uri::current(); ``` #### Retrieving URI Components[​](#retrieving-uri-components "Direct link to Retrieving URI Components") Once you have a URI instance, you can access its individual components: ```php $uri = uri( 'https://user:pass@example.com:8080/path/to/page?query=value#fragment' ); $scheme = $uri->scheme(); // 'https' $user = $uri->user(); // 'user' $userWithPass = $uri->user( true ); // 'user:pass' $password = $uri->password(); // 'pass' $host = $uri->host(); // 'example.com' $port = $uri->port(); // 8080 $path = $uri->path(); // '/path/to/page' $segments = $uri->path_segments(); // Collection: ['path', 'to', 'page'] $query = $uri->query(); // Uri_Query_String instance $queryParams = $uri->query()->all(); // ['query' => 'value'] $fragment = $uri->fragment(); // 'fragment' ``` tip The `path()` method always returns a path with a leading slash, and empty paths are returned as a single `/`. #### Modifying URIs[​](#modifying-uris "Direct link to Modifying URIs") The `Uri` class provides a fluent interface for modifying components. Each modification method returns a new instance, so the original URI remains unchanged: ```php use Mantle\Support\Uri; $uri = uri( 'https://example.com/path' ); // Change the scheme $secureUri = $uri->with_scheme( 'https' ); // Change the host $newHost = $uri->with_host( 'mantle-framework.com' ); // Change the path $newPath = $uri->with_path( '/new/path' ); // Add a port $withPort = $uri->with_port( 8080 ); // Add user credentials $withAuth = $uri->with_user( 'username', 'password' ); // Add a fragment $withFragment = $uri->with_fragment( 'section-1' ); ``` ##### Working with Query Parameters[​](#working-with-query-parameters "Direct link to Working with Query Parameters") The `Uri` class provides several methods to work with query parameters: ```php use function Mantle\Support\Helpers\uri; $uri = uri( 'https://example.com/search?term=php' ); // Add or merge query parameters $withFilters = $uri->with_query( ['category' => 'framework', 'sort' => 'desc'] ); // https://example.com/search?term=php&category=framework&sort=desc // Replace all query parameters $newQuery = $uri->replace_query( ['page' => 1, 'limit' => 20] ); // https://example.com/search?page=1&limit=20 // Add query parameters only if they don't exist $withDefaults = $uri->with_query_if_missing( ['term' => 'default', 'page' => 1] ); // https://example.com/search?term=php&page=1 // Remove specific query parameters $withoutTerm = $uri->without_query( 'term' ); // https://example.com/search // Remove all query parameters $withoutQuery = $uri->without_query(); // https://example.com/search // Push a value onto a list parameter $uri = uri( 'https://example.com/search?tags[]=php' ); $withMoreTags = $uri->push_onto_query( 'tags', ['framework', 'web'] ); // https://example.com/search?tags[]=php&tags[]=framework&tags[]=web ``` #### Additional Features[​](#additional-features "Direct link to Additional Features") ##### Creating Redirect Responses[​](#creating-redirect-responses "Direct link to Creating Redirect Responses") The `Uri` class can generate redirect HTTP responses: ```php use function Mantle\Support\Helpers\uri; // Create a redirect response with default 302 status $response = uri( 'https://example.com/destination' )->redirect(); // Create a redirect with a specific status code and headers $response = uri( 'https://example.com/destination' )->redirect( 301, ['X-Redirect-Source' => 'application'] ); ``` ##### Converting to String[​](#converting-to-string "Direct link to Converting to String") URIs can be converted to strings in several ways: ```php use function Mantle\Support\Helpers\uri; $uri = uri( 'https://example.com/path?query=value#fragment' ); // Using the value() method $string = $uri->value(); // Using the __toString() method $string = (string) $uri; // Decode the URI components $decoded = $uri->decode(); ``` ##### Debugging[​](#debugging "Direct link to Debugging") The `Uri` class provides a `dump()` method for debugging: ```php // Dump the URI string and continue $uri->dump(); ``` ##### Conditional Methods[​](#conditional-methods "Direct link to Conditional Methods") The `Uri` class includes the `Conditionable` trait, which allows for conditional modifications: ```php use function Mantle\Support\Helpers\uri; $uri = uri( 'https://example.com/path' ); $result = $uri->when( $condition, function ( $uri ) { return $uri->with_path( '/conditional/path' ); }, function ( $uri ) { return $uri->with_path( '/default/path' ); } ); ``` #### Practical Examples[​](#practical-examples "Direct link to Practical Examples") ##### Building an API URL[​](#building-an-api-url "Direct link to Building an API URL") ```php $apiUrl = uri('https://api.example.com') ->with_path('/v1/products') ->with_query([ 'category' => 'electronics', 'limit' => 20, 'page' => 1, 'sort' => 'price', 'order' => 'asc', ]); ``` ##### Generating Pagination URLs[​](#generating-pagination-urls "Direct link to Generating Pagination URLs") ```php $baseUrl = uri()->without_query('page'); $pagination = [ 'current' => $current_page, 'next' => $baseUrl->with_query(['page' => $current_page + 1])->value(), 'prev' => $current_page > 1 ? $baseUrl->with_query(['page' => $current_page - 1])->value() : null, ]; ``` ##### Preserving Selected Filters[​](#preserving-selected-filters "Direct link to Preserving Selected Filters") ```php $current = uri(); $availableFilters = ['category', 'price', 'color', 'size']; // Keep existing filters and update only the ones that changed $updatedUri = $current->with_query([ 'category' => $new_category, 'page' => 1, // Reset to page 1 when filters change ]); ``` ````php use function Mantle\Support\Helpers\uri; ## Practical Examples ### Building an API URL ```php use function Mantle\Support\Helpers\uri; $apiUrl = uri( 'https://api.example.com' ) ->with_path( '/v1/products' ) ->with_query( [ 'category' => 'electronics', 'limit' => 20, 'page' => 1, 'sort' => 'price', 'order' => 'asc', ] ); // Make an API request using the built URL $response = http()->get( $apiUrl ); ```` ##### Generating Pagination URLs[​](#generating-pagination-urls-1 "Direct link to Generating Pagination URLs") ```php use function Mantle\Support\Helpers\uri; $baseUrl = uri()->without_query( 'page' ); $pagination = [ 'current' => $current_page, 'next' => $baseUrl->with_query( ['page' => $current_page + 1] )->value(), 'prev' => $current_page > 1 ? $baseUrl->with_query( ['page' => $current_page - 1] )->value() : null, ]; ``` ##### Preserving Selected Filters[​](#preserving-selected-filters-1 "Direct link to Preserving Selected Filters") ```php use function Mantle\Support\Helpers\uri; $current = uri(); $availableFilters = ['category', 'price', 'color', 'size']; // Keep existing filters and update only the ones that changed $updatedUri = $current->with_query( [ 'category' => $new_category, 'page' => 1, // Reset to page 1 when filters change ] ); ``` ##### Generating Pagination URLs[​](#generating-pagination-urls-2 "Direct link to Generating Pagination URLs") ```php use function Mantle\Support\Helpers\uri; $baseUrl = uri()->without_query('page'); $pagination = [ 'current' => $current_page, 'next' => $baseUrl->with_query(['page' => $current_page + 1])->value(), 'prev' => $current_page > 1 ? $baseUrl->with_query(['page' => $current_page - 1])->value() : null, ]; ``` ##### Preserving Selected Filters[​](#preserving-selected-filters-2 "Direct link to Preserving Selected Filters") ```php use function Mantle\Support\Helpers\uri; $current = uri(); $availableFilters = ['category', 'price', 'color', 'size']; // Keep existing filters and update only the ones that changed $updatedUri = $current->with_query([ 'category' => $new_category, 'page' => 1, // Reset to page 1 when filters change ]); ``` #### The uri() Helper Function[​](#the-uri-helper-function "Direct link to The uri() Helper Function") Mantle provides a convenient `uri()` helper function to quickly create `Uri` instances: ```php /** * Create a new Uri object from the given URI string. * If no URI is provided, it will capture the current request URI. * * @param string|null $uri The URI to create the Uri object from. Defaults to the current request URI. */ function uri( ?string $uri = null ): Uri { return $uri ? Uri::of( $uri ) : Uri::current(); } ``` This helper function simplifies the process of creating URI instances and makes your code more readable. --- ### Types The types package is an extension to the [`alleyinteractive/wp-type-extensions`](https://github.com/alleyinteractive/wp-type-extensions) package. The feature pattern is used by Mantle and throughout Alley projects to define features on a project. It provides a consistent way to register and manage features that promotes modularity and reusability. Lets look at an example of a feature: ```php use Alley\WP\Types\Feature; class My_Custom_Type_Feature extends Feature { public function boot(): void { // Register custom types here. } } ``` The `My_Custom_Type_Feature` class extends the `Feature` interface, which requires the implementation of a `boot()` method. This method is called when the feature is initialized. Features can be booted in a group using the `Group` class: ```php use Alley\WP\Features\Group; $group = new Group( new My_Custom_Type_Feature(), ); $group->boot(); ``` For more information about the `wp-type-extensions` project, see [the project's README](https://github.com/alleyinteractive/wp-type-extensions/blob/main/README.md). #### Hookable Feature[​](#hookable-feature "Direct link to Hookable Feature") The `Hookable_Feature` class implements the `Feature` interface and uses the [Hookable trait](/docs/features/support/hookable.md) to provide a convenient way to register WordPress hooks within a feature. ```php use Mantle\Support\Attributes\Action; use Mantle\Types\Hookable_Feature; class Example_Feature extends Hookable_Feature { #[Action('init')] public function register_custom_type(): void { // Register custom types here. } } ``` You can then use the feature normally within a feature group. ```php use Alley\WP\Features\Group; $group = new Group( new Example_Feature(), ); $group->boot(); ``` The Hookable trait will add the `register_custom_type` method as a callback for the `init` action when the feature is booted. #### Validator Group[​](#validator-group "Direct link to Validator Group") The `Validator_Group` class allows you boot features in a group with run-time validation. For example, you can add all the possible features to be booted to a validator group, and then only the features that pass validation will be booted. Validation is done using attributes that implement the `Mantle\Types\Validator` interface. Features that include the attribute will only be booted if the attribute's `validate` method returns true. Features that do not include any validator attributes will always be booted. Let's look at an example custom validator attribute: ```php use Alley\WP\Types\Feature; #[\Attribute] class Example_Validator implements \Mantle\Types\Validator { public function validate(): bool { return wp_rand(0, 1) === 1; } } ``` You can then use the validator attribute on a feature: ```php use Mantle\Support\Attributes\Action; use Mantle\Types\Hookable_Feature; #[Example_Validator] class Example_Feature extends Hookable_Feature { #[Action('init')] public function register_custom_type(): void { // Register custom types here. } } ``` To put it all together, the validator group will be used to selectively boot the features: ```php use Mantle\Types\Validator_Group; $group = new Validator_Group( new Example_Feature(), new \Alley\WP\Features\Quick_Feature( function (): void { // This feature will always be booted because it has no validators. } ), ); $group->boot(); ``` The `Example_Feature` will only be booted if the `Example_Validator` returns true. #### Validator Attribute[​](#validator-attribute "Direct link to Validator Attribute") A validator attribute is a PHP attribute that implements the `Mantle\Types\Validator` interface. The attribute's `validate` method is called when the class is being initialized to determine if the feature should be booted. Here's an example of a custom validator attribute: ```php class Example_Validator implements \Mantle\Types\Validator { public function validate(): bool { // Return true to boot the feature, false to skip it. } } ``` ##### Available Validators[​](#available-validators "Direct link to Available Validators") The following validator attributes are available: ###### `CLI`[​](#cli "Direct link to cli") The `Mantle\Types\Attributes\CLI` validator attribute will only allow the feature to be booted if the current request is from the command line interface (CLI). ```php use Alley\WP\Types\Feature; use Mantle\Types\Attributes\CLI; #[CLI] class CLI_Only_Feature extends Feature { public function boot(): void { // This feature will only be booted when running in the CLI. } } ``` ###### `Environment`[​](#environment "Direct link to environment") The `Mantle\Types\Attributes\Environment` validator attribute will only allow the feature to be booted if the current environment matches one of the specified environments. One or more environment names can be passed to the attribute. ```php use Alley\WP\Types\Feature; use Mantle\Types\Attributes\Environment; #[Environment('production', 'staging')] class Production_Staging_Feature extends Feature { public function boot(): void { // This feature will only be booted in the 'production' or 'staging' environments. } } ``` --- ### Mantle Welcome to the Mantle—a modern and powerful toolkit built atop WordPress to streamline and elevate enterprise WordPress applications. Whether you’re an experienced developer or new to enterprise-level WordPress development, Mantle provides a structured, flexible foundation for building complex applications, without sacrificing the extensibility and accessibility of WordPress. Mantle always uses the same underlying WordPress APIs and functions you know and love. It doesn't replace WordPress, but rather enhances it with modern development practices and tools. #### What is Mantle?[​](#what-is-mantle "Direct link to What is Mantle?") Mantle is an object-oriented framework designed to enhance WordPress for enterprise projects by providing essential abstractions, utility functions, and best practices for developing clean, maintainable code. Built to optimize performance, scalability, and developer productivity, Mantle bridges the gap between WordPress’s approachable CMS features and the advanced requirements of modern web applications. Mantle brings a Laravel-inspired flexibility to WordPress, bringing the latest and greatest to WordPress development. It provides a structured, organized framework for building complex applications, while still leveraging the extensibility and accessibility of WordPress. ##### Provides Models for Interacting with Data[​](#provides-models-for-interacting-with-data "Direct link to Provides Models for Interacting with Data") Mantle provides a robust model toolkit for interacting with data in WordPress. Models allow you to interact with your database in a more object-oriented way and removes most of the quirks of dealing with WordPress' API (attributes passed to `wp_insert_post()` for example). ```php use Mantle\Database\Model\Post; $post = Post::create( [ 'post_title' => 'My New Post', 'post_content' => 'This is the content of my new post.', ] ); $post->post_content = 'This is an updated content.'; $post->save( [ 'post_title' => 'My Updated Post', ] ); // You can also query for posts. $posts = Post::where( 'post_title', 'Example Title' )->get(); ``` For more information about models, see the [documentation](/docs/models.md). ##### Adds Blade Templating[​](#adds-blade-templating "Direct link to Adds Blade Templating") Mantle allows you to use Blade templating in your WordPress project. Blade is a powerful templating engine that allows you to write clean, concise templates without the clutter of PHP tags. ExampleController.php ```php class ExampleController extends Controller { public function index() { $posts = Post::all(); return view('posts', ['posts' => $posts]); } } ``` resources/views/posts.blade.php ```php
@foreach ($posts as $post)

{{ $post->title }}

{{-- Display the post image if it exists --}} @if ($post->image_url) {{ $post->title }} image @endif

{{ $post->content }}

@endforeach
``` For more information about blade and other templating, see the [documentation](/docs/basics/templating.md). ##### Better Routing[​](#better-routing "Direct link to Better Routing") Mantle provides a fluent routing system that allows you to define custom routes for your WordPress application. This makes it easy to create custom endpoints for your application. It also provides a way to define REST API routes in a structured way. routes/web.php ```php use Mantle\Facade\Route; Route::get( '/posts', 'PostController@index' ); Route::get( '/post/{post}', function ( Post $post ) { return view( 'post' )->with( 'post', $post ); } ); Route::post( '/upload/', function ( Request $post ) { $attachment_id = $request->file( 'uploaded_image' )->store_as_attachment(); return response()->json( [ 'attachment_id' => $attachment_id, 'message' => 'Image uploaded successfully', ] ); } ); Route::rest_api( 'namespace/v1', '/route-to-use', function() { return [ ... ]; } ); ``` For more information about fluent routing, see the [documentation](/docs/basics/requests.md). ##### Independent Testing Framework[​](#independent-testing-framework "Direct link to Independent Testing Framework") Mantle provides a isolated testing framework that allows you to write tests for your WordPress application. Similar to Laravel, you can use Mantle's testing framework to perform HTTP request inspection, database seeding, and more. ```php use App\Tests\TestCase; class ExampleTest extends TestCase { public function test_example(): void { $post = static::factory()->post->create_and_get(); $this->get( $post ) ->assertOk() ->assertSee( $post->post_title ); $this->post( '/upload', [ 'uploaded_file' => [ ... ], ] ) ->assertStatus( 201 ) ->assertJsonPath( 'message', 'Image uploaded successfully' ); } } ``` For more information about testing, see the [documentation](/docs/testing.md). #### Who is Mantle For?[​](#who-is-mantle-for "Direct link to Who is Mantle For?") Mantle is for developers and technical teams working with WordPress in environments where scalability, flexibility, and reliability are paramount. If your project involves complex data modeling, customized RESTful APIs, advanced caching, or intricate frontend requirements, Mantle equips you with the tools to build with confidence. * Agencies and In-House Development Teams building bespoke WordPress applications for enterprise clients. Mantle can be used to build new projects from scratch or to enhance existing ones. * Advanced WordPress Developers seeking a cleaner, more organized framework for handling complex applications with DRY and MVP principles. * Product Owners looking to streamline development processes, reduce technical debt, and ensure maintainable codebases. #### How can I get started with Mantle?[​](#how-can-i-get-started-with-mantle "Direct link to How can I get started with Mantle?") To get started with Mantle, continue with the [installation guide](/docs/getting-started/installation.md). --- ### Directory Structure ### Overview Below is the current proposed directory structure for sites using Mantle. The tree is assumed to be placed inside of a `wp-content/plugin/{site}` folder. ```text . ├── README.md ├── app │   ├── console │   │   └── class-example-command.php │   ├── jobs │   │   └── class-example-job.php │   ├── models │   │   └── class-example-post.php │   └── providers │   └── class-app-service-provider.php ├── bootstrap │   └── app.php ├── composer.json ├── config │   └── app.php ├── database │   ├── factories │   │   └── class-post-factory.php │   └── seeds │   └── class-database-seeder.php ├── routes │   ├── cli.php │   ├── rest-api.php │   └── web.php └── tests ├── class-test-case.php ├── feature │   └── test-example.php └── unit └── test-example.php ``` ### Root Directory #### The App Directory[​](#the-app-directory "Direct link to The App Directory") The `app` directory contains the core code of your application. This includes console commands, routes, models, providers, and more. Most of the application will live inside of this folder. By default this folder is namespaced `App` and autoloaded using a WordPress-style autoloader. #### The Bootstrap Directory[​](#the-bootstrap-directory "Direct link to The Bootstrap Directory") The `bootstrap` directory contains the `app.php` file which bootstraps and loads the framework. It can also contain a `cache` folder which contains framework generated files for performance optimization including routes and packages. #### The Config Directory[​](#the-config-directory "Direct link to The Config Directory") The `config` directory contains the application configuration. For more information about this, read the Configuration documentation page. #### The Database Directory[​](#the-database-directory "Direct link to The Database Directory") The `database` directory contains the database factories and seeders used to initialize the database for testing. For more information about this, read the 'Model Factory' documentation page. #### The Routes Directory[​](#the-routes-directory "Direct link to The Routes Directory") The `routes` directory contains all of the application's HTTP route definitions. By default, this includes `web.php` and `rest-api.php` for web and REST API routes, respectively. #### The Tests Directory[​](#the-tests-directory "Direct link to The Tests Directory") The `tests` directory contains the automated tests for the application powered by PHPUnit and the Mantle Test Framework. --- ### Installation #### Requirements[​](#requirements "Direct link to Requirements") Mantle has two system requirements to run: * PHP to be at least running 8.2. Mantle supports PHP 8.2 to 8.5 as of Mantle v1.5. * WordPress to be at least 6.5. #### How Can I Use Mantle?[​](#how-can-i-use-mantle "Direct link to How Can I Use Mantle?") Mantle supports two different modes of operation: 1. **This is the most common setup:** As a plugin/theme framework for a WordPress site. It normally lives in `wp-content/plugins/mantle.` and uses [`alleyinteractive/mantle`](https://github.com/alleyinteractive/mantle) as the starter package for this mode. 2. In isolation in an existing code base. The Mantle framework provides all it's own default configuration and service providers and doesn't require most of the code in `alleyinteractive/mantle` to function. This mode is useful for integrating Mantle into an existing code base and using one of its features like queueing or routing without setting up the rest of the application. For more information on this mode, see the [Using Mantle Without a Starter Template](#using-without-a-starter-template) section. #### Installing Mantle on a Site with Mantle Installer[​](#installing-mantle-on-a-site-with-mantle-installer "Direct link to Installing Mantle on a Site with Mantle Installer") Mantle sites should live in `wp-content/plugins/{site-slug}/` inside a WordPress project. The Mantle Installer can install Mantle on a new or existing WordPress application. The installer can be installed via a PHP PHAR or globally using Composer (PHAR is recommended). ##### Via PHAR 📦[​](#via-phar- "Direct link to Via PHAR 📦") Download the latest PHAR release of the Mantle installer from the [latest releases page](https://github.com/alleyinteractive/mantle-installer/releases/latest). You can use the PHAR directly or move to a `$PATH` directory. Here's a quick example of how to do all of that: ```bash # Download the latest PHAR release. gh release download --clobber -p '*.phar' -R alleyinteractive/mantle-installer chmod +x mantle-installer.phar # Optionally move it to a directory in your PATH for global usage. sudo mv mantle-installer.phar /usr/local/bin/mantle ``` note This example uses the `gh` CLI from GitHub to download the latest release. You can also download the PHAR manually from the [latest release page](https://github.com/alleyinteractive/mantle-installer/releases/latest). From here, you can run the `mantle` command from anywhere on your system: ```text Mantle Installer Usage: command [options] [arguments] Options: -h, --help Display help for the given command. When no command is given display help for the list command --silent Do not output any message -q, --quiet Only errors are displayed. All other output is suppressed -V, --version Display this application version --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: completion Dump the shell completion script help Display help for a command list List commands new Create a new Mantle application ``` ##### Via Composer 🛠 ️[​](#via-composer--️ "Direct link to Via Composer 🛠 ️") Download the Mantle installer using [Composer](https://getcomposer.org/). ```bash composer global require alleyinteractive/mantle-installer ``` Once installed the `mantle new` command will create a fresh Mantle installation wherever you specify. It can also install WordPress for you or install Mantle into an existing WordPress installation. ```bash mantle new my-site ``` ##### Manual Installation[​](#manual-installation "Direct link to Manual Installation") Alternatively, you can install a Mantle site using Composer, replacing `my-site` with your site's slug. ```bash cd wp-content/plugins/ composer create-project alleyinteractive/mantle my-site \ --remove-vcs \ --stability=dev \ --no-cache \ --no-interaction ``` #### Starter Template for `wp-content`-based Projects[​](#starter-template-for-wp-content-based-projects "Direct link to starter-template-for-wp-content-based-projects") [Alley's `create-wordpress-project`](https://github.com/alleyinteractive/create-wordpress-project) starter template can be used as a starting point for using Mantle on a `wp-content`-rooted project. `create-wordpress-project` includes a configuration script to help you setup your project and install plugins and themes. It also supports installing Mantle as a plugin out of the box. #### Using Without a Starter Template[​](#using-without-a-starter-template "Direct link to Using Without a Starter Template") Mantle supports the use of the framework and its features in complete isolation, without the need of the starter code in `alleyinteractive/mantle`. Using the Application Bootloader, you can instantiate the Mantle framework in one line and use it's features in any code base. ```php bootloader()->boot(); ``` tip For more information on the Bootloader, see the [Bootloader documentation](/docs/architecture/bootloader.md). For example, if you want to use [Mantle's Queue feature](/docs/features/queue.md) in an existing code base, you can do so by booting the framework and then using the `dispatch()` helper (see the [queue documentation](/docs/features/queue.md) for more information). ```php // Boot the application. bootloader()->boot(); // Dispatch an anonymous job. dispatch( function () { // Do something expensive here. } ); ``` Calling the bootloader is all you need to use Mantle in isolation -- no other files or directories need to be created. If you want to enhance your experience, you can copy files down from [alleyinteractive/mantle](https://github.com/alleyinteractive/mantle) as needed to your code base. --- ### Tutorial #### Introduction[​](#introduction "Direct link to Introduction") This quick-start guide aims to provide a basic introduction to Mantle and its features. This is a great starting point if you are brand new to Mantle and even WordPress alike. #### Installation[​](#installation "Direct link to Installation") info This guide assumes that you are running WordPress in a Vagrant or similar local PHP environment such as [VVV](https://varyingvagrantvagrants.org/). Download the Mantle installer using [Composer](https://getcomposer.org/). ```bash composer global require alleyinteractive/mantle-installer ``` Once downloaded, change your directory to a folder that is shared with your Vagrant machine. For example if `~/web` is shared with `/var/www` on my machine, switch to `~/web`. Run the installer: ```bash mantle new my-project -i ``` Once that is complete you will have WordPress installed at `~/web/my-project`. Open up your browser and navigate to `my-project.test` (or whatever web host you setup) to complete installation. #### Open Your Editor[​](#open-your-editor "Direct link to Open Your Editor") Once installation is complete you can open up your editor. Open the folder your just installed `~/web/my-project` and navigate to the Mantle plugin in `wp-content/plugins/my-project`. #### Create a Post Type Model[​](#create-a-post-type-model "Direct link to Create a Post Type Model") Scaffold a new `project` post type using [`wp-cli`](https://wp-cli.org/). ```bash wp mantle make:model Project --model_type=post --registrable ``` The console command should have succeeded and told you to include the model in your configuration file. Go ahead and open up `config/models.php` inside of your Mantle plugin and add `App\Models\project::class` to the array there. It should look like this: ```php [ App\Models\Project::class ], ]; ``` #### Creating a Factory[​](#creating-a-factory "Direct link to Creating a Factory") Instead of having to open up `wp-admin` and create data yourself, we can use [Mantle's Factories](/docs/models/database-factory.md) to create data for us. Open up your terminal and use the `make:factory` command: ```bash wp mantle make:factory Project --model_type=post ``` That command will create a factory for your project in `database/factories/project-factory.php`. Open that file up and modify the factory's definition for the project post type to include some additional meta data. tip Factories can use the [Faker](https://github.com/fzaninotto/Faker) package to create "real" data quickly. ```php */ class Project_Factory extends \Mantle\Database\Factory\Post_Factory { /** * Model to use when creating objects. * * @var class-string */ protected string $model = Project::class; /** * Define the model's default state. * * @return array */ public function definition(): array { return [ 'post_title' => $this->faker->sentence, 'post_content' => $this->faker->paragraph, 'post_status' => 'publish', 'post_type' => '{{ object_name }}', ]; } } ``` #### Seeding Some Data[​](#seeding-some-data "Direct link to Seeding Some Data") By default Mantle includes a seeder in every project in the `/database/seeds/class-database-seeder.php` file. Let's open that file up and include our new factory. ```php create_many( 10 ); } } ``` With that added, lets run the database seeder. Inside of your console you can run: ```bash wp mantle db:seed ``` If you open up your WordPress admin and navigate to the Projects post type you'll now see 10 new project posts. #### Creating a Route[​](#creating-a-route "Direct link to Creating a Route") Next, we're ready to add a route to view that new post type. Web routes are stored in `routes/web.php` by default. For our purposes, we'll create a new route to view the `Project` model and wrap it in the `web` middleware. Inside of `routes/web.php`, create two routes: one to list projects and another to view a specific project. tip The second route uses PHP type-hinting to automatically resolve the model instance with [implicit model binding](/docs/basics/requests.md#implicit-binding). ```php Route::get( '/projects', function() { return response()->view( 'list', [ 'projects' => Project::all() ] ); } ); Route::get( '/project/{project}', function( Project $project ) { return response()->view( 'single', [ 'title' => $project->title, 'owner' => $project->meta->project_owner, 'email' => $project->meta->project_email, ] ) } ); ``` #### Creating a View[​](#creating-a-view "Direct link to Creating a View") Mantle supports using Blade templates as well as normal PHP templates in your application. Create a new view at `plugins/my-project/views/single.blade.php`: ```php

{{$title}}

Project Owner: {{$owner}}

Project Email: {{$email}}

``` Additionally create a normal PHP template at `wp-content/plugins/my-project/views/list.php`: ```php ``` You are now able to open up you browser and go to `/projects` on your project's website (`https://my-project.test/projects` for example). --- ### Models Models provide a fluent way to interface with objects in WordPress. Models can be either a post type, a term, or a subset of a post type. They represent a data object inside of your application. Models can also allow dynamic registration of a post type, REST API fields, and more. To make life easier for developers, Mantle models were designed with uniformity and simplicity in mind. tip The Mantle Framework is not required to use Mantle models or the [query builder](/docs/models/query-builder.md). You can require the package in your application with Composer and use the models and query builder in your application. ```bash composer require mantle-framework/database ``` #### Supported Model Types[​](#supported-model-types "Direct link to Supported Model Types") The common data structures in WordPress are supported as models: | Data Type | Model Class | | ---------- | ---------------------------------- | | Attachment | `Mantle\Database\Model\Attachment` | | Comment | `Mantle\Database\Model\Comment` | | Post | `Mantle\Database\Model\Post` | | Site | `Mantle\Database\Model\Site` | | Term | `Mantle\Database\Model\Term` | | User | `Mantle\Database\Model\User` | #### Generating a Model[​](#generating-a-model "Direct link to Generating a Model") Models can be generated through the command: ```bash bin/mantle make:model --model_type= [--registrable] [--object_name] [--label_singular] [--label_plural] ``` #### Defining a Model[​](#defining-a-model "Direct link to Defining a Model") Models live in the `app/models` folder under the `App\Models` namespace. ##### Example Post Model[​](#example-post-model "Direct link to Example Post Model") ```php /** * Example_Model class file. * * @package App\Models */ namespace App\Models; use Mantle\Database\Model\Post; /** * Example_Model Model. */ class Example_Model extends Post { /** * Post Type * * @var string */ public static $object_name = 'example-model'; } ``` ##### Example Term Model[​](#example-term-model "Direct link to Example Term Model") ```php /** * Example_Model class file. * * @package App\Models */ namespace App\Models; use Mantle\Database\Model\Term; /** * Example_Model Model. */ class Example_Model extends Term { /** * Term Type * * @var string */ public static $object_name = 'example-model'; } ``` #### Creating a Dynamic Model[​](#creating-a-dynamic-model "Direct link to Creating a Dynamic Model") A dynamic model is a model that is not defined in the application but is created on the fly. This is useful for creating a model for a post type that isn't defined in the application. Once the dynamic model is created, it can be used like any other model in the application. ```php use Mantle\Database\Model\Post; $model = Post::for( 'my-custom-post-type' ); // Create a new instance of the model. $instance = $model->create( [ 'title' => 'Example Title' ] ); // Query the model. $results = $model->where( 'post_status', 'publish' )->get(); ``` #### Interacting with Models[​](#interacting-with-models "Direct link to Interacting with Models") Setting/updating data with a model can be done using the direct attribute name you wish to update or a handy alias (see [Core Object](#core-object)). ```php $post->content = 'Content to set.'; // is the same as... $post->post_content = 'Content to set.'; // Save the post. $post->save(); ``` ##### Field Aliases[​](#field-aliases "Direct link to Field Aliases") ###### Post Aliases[​](#post-aliases "Direct link to Post Aliases") | Alias | Field | | -------------- | ------------------- | | `content` | `post_content` | | `date` | `post_date` | | `date_gmt` | `post_date_gmt` | | `modified` | `post_modified` | | `modified_gmt` | `post_modified_gmt` | | `description` | `post_excerpt` | | `id` | `ID` | | `title` | `post_title` | | `name` | `post_title` | | `slug` | `post_name` | | `status` | `post_status` | ###### Term Aliases[​](#term-aliases "Direct link to Term Aliases") | Alias | Field | | ----- | --------- | | `id` | `term_id` | ##### Saving/Updating Models[​](#savingupdating-models "Direct link to Saving/Updating Models") The `save()` method on the model will store the data for the respective model. It also supports an array of attributes to set for the model before saving. ```php $post->content = 'Content to set.'; $post->save(); $post->save( [ 'content' => 'Fresher content' ] ); ``` ##### Deleting Models[​](#deleting-models "Direct link to Deleting Models") The `delete()` method on the model will delete the model. On `Post` models, the delete method will attempt to trash the post if the post type supports it. You can pass `$force = true` to bypass that. ```php // Force to delete it. $post->delete( true ); $term->delete(); ``` ##### Interacting with Dates[​](#interacting-with-dates "Direct link to Interacting with Dates") Post models support setting both the published and modified dates. The dates are expected to be datetime strings. ```php $post->created = '2021-01-01 12:00:00'; $post->modified = '2021-01-01 12:00:00'; // Save the post. $post->save(); ``` You can also retrieve and set the dates as `Carbon`/`DateTimeInterface` objects using the `dates` attribute. ```php use Carbon\Carbon; // Retrieve the created date. $created = $post->dates->created; // Carbon\Carbon $created_gmt = $post->dates->created_gmt; // Carbon\Carbon // Retrieve the modified date. $modified = $post->dates->modified; // Carbon\Carbon $modified_gmt = $post->dates->modified_gmt; // Carbon\Carbon // Set the created date as a Carbon object. $post->dates->created = now()->subDay(); // Set the created date as a string. $post->dates->created = '2021-01-01 12:00:00'; // Save the post. $post->save(); ``` You can get/set the published date using the `created` attribute and the modified date using the `modified` attribute. ##### Interacting with Meta[​](#interacting-with-meta "Direct link to Interacting with Meta") The `Post`, `Term`, and `User` model types support setting meta easily. Models support a fluent way of setting meta using the `meta` attribute. The meta will be queued for saving and saved once you call the `save()` method on the model: ```php use App\Models\Post; $model = Post::factory()->create(); // Retrieve meta value. $value = $model->meta->meta_key; // mixed // Update a meta value as an attribute. $model->meta->meta_key = 'meta-value'; $model->save(); // Delete a meta key. unset( $model->meta->meta_key ); $model->save(); ``` The same syntax is supported for interacting with meta as an array (no preference to use one over the other): ```php use App\Models\Post; $model = Post::factory()->create(); // Retrieve meta value. $value = $model->meta['meta_key']; // mixed // Update a meta value as an attribute. $model->meta['meta_key'] = 'meta-value'; $model->save(); // Delete a meta key. unset( $model->meta['meta_key'] ); $model->save(); ``` When interacting with the `meta` attribute, the meta will not be saved until the model is saved. This allows you to set multiple meta values before saving the model. You can also interact with meta directly using the `get_meta`, `set_meta`, and `delete_meta` methods: ```php use App\Models\Post; $model = Post::factory()->create(); // Meta will be stored immediately unless the model hasn't been saved yet // (allows you to set meta before saving the post). $model->set_meta( 'meta-key', 'meta-value' ); $model->delete_meta( 'meta-key' ); $value = $model->get_meta( 'meta-key' ); // mixed // Meta can also be saved directly with the save() method. $model->save( [ 'meta' => [ 'meta-key' => 'meta-value' ] ] ); ``` note These methods will automatically update the model's meta without needing to call the `save()` method. If a model is not saved yet an exception will be thrown. ##### Interacting with Terms[​](#interacting-with-terms "Direct link to Interacting with Terms") The `Post` model support interacting with terms through [relationships](/docs/models/model-relationships.md) or through the model directly. The model supports multiple methods to make setting terms on a post simple: ```php use App\Models\Post; $category = Category::whereName( 'Example Category' )->first(); // Save the category to a post. $post = new Post( [ 'title' => 'Example Post' ] ); // Also supports an array of IDs or WP_Term objects. $post->terms->category = [ $category ]; $post->save(); // Read the tags from a post. $post->terms->post_tag // Term[] ``` Terms can also be set when creating a post (specifying the taxonomy is optional): ```php use App\Models\Post; $post = new Post( [ 'title' => 'Example Title', 'terms' => [ $category ], ] ); $post = new Post( [ 'title' => 'Example Title', 'terms' => [ 'category' => [ $category ], 'post_tag' => [ $tag ], ], ] ); ``` Models also support simpler `get_terms`/`set_terms` methods for function based setting of a post's terms: ```php use App\Models\Post; $post = Post::find( 1234 ); // Set the terms on a model. $post->set_terms( [ $terms ], 'category' ); // Read the terms on a model. $post->get_terms( 'category' ); // Term[] ``` #### Core Object[​](#core-object "Direct link to Core Object") To promote a uniform interface of data across models, all models implement `Mantle\Contracts\Database\Core_Object`. This provides a consistent set of methods to invoke on any model you may come across. A developer shouldn't have to check the model type before retrieving a field. This helps promote interoperability between model types in your application. The `core_object()` method will retrieve the WordPress core object that the model represents (`WP_Post` for a post, `WP_Term` for a term, and so on). ```php id(): int name(): string slug(): string description(): string parent(): ?Core_Object permalink(): ?string core_object(): object|null ``` #### Events[​](#events "Direct link to Events") In the spirit of interoperability, you can listen to model events in a uniform way across all model types. Currently only `Post` and `Term` models support events. Model events can be registered inside or outside a model. One common place to register events is in the `boot` method of a model: ```php namespace App\Models; use Mantle\Database\Model\Post as Base_Post; class Post extends Base_Post { protected static function boot() { static::created( function( $post ) { // Fired after the model is created. } ); } } ``` ##### Supported Events[​](#supported-events "Direct link to Supported Events") ###### created[​](#created "Direct link to created") Fired after a model is created and exists in the database. ```php use App\Models\Post; Post::created( function( $post ) { // Fired after the model is created. } ); ``` ###### updating[​](#updating "Direct link to updating") Fired before a model is updated. ```php use App\Models\Post; Post::updating( function( $post ) { // Fired before the model is updated. } ); ``` ###### updated[​](#updated "Direct link to updated") Fired after a model is updated. ```php use App\Models\Post; Post::updated( function( $post ) { // Fired after the model is updated. } ); ``` ###### deleting[​](#deleting "Direct link to deleting") Fired before a model is deleted. ```php use App\Models\Post; Post::deleting( function( $post ) { // Fired before the model is deleted. } ); ``` ###### deleted[​](#deleted "Direct link to deleted") Fired after a model is deleted. ```php use App\Models\Post; Post::deleted( function( $post ) { // Fired after the model is deleted. } ); ``` ###### trashing[​](#trashing "Direct link to trashing") Fired before a model is trashed. ```php use App\Models\Post; Post::trashing( function( $post ) { // Fired before the model is trashed. } ); ``` ###### trashed[​](#trashed "Direct link to trashed") Fired after a model is trashed. ```php use App\Models\Post; Post::trashed( function( $post ) { // Fired after the model is trashed. } ); ``` note Events do require the Mantle Framework to be instantiated if you are using models in isolation. #### Query Scopes[​](#query-scopes "Direct link to Query Scopes") A scope provides a way to add a constraint to a model's query easily. ##### Global Scope[​](#global-scope "Direct link to Global Scope") Using a global scope, a model can become a subset of a parent model. For example, a `User` model can be used to define a user object while an `Admin` model can describe an admin user. Underneath they are both user objects but the `Admin` model allows for an easier interface to retrieve a subset of data. In the example below, the `Admin` model extends itself from `User` but includes a meta query in all requests to the model (`is_admin = 1`). ```php use App\Models\User; use Mantle\Database\Query\Post_Query_Builder; class Admin extends User { protected static function boot() { static::add_global_scope( 'scope-name', function( Post_Query_Builder $query ) { return $query->whereMeta( 'is_admin', '1' ); } ) } } ``` Global Scopes can also extend from a class to allow for reusability. ```php use App\Models\User; use Mantle\Contracts\Database\Scope; use Mantle\Database\Model\Model; class Admin extends User { protected static function boot() { parent::boot(); static::add_global_scope( new Test_Scope() ); } } class Admin_Scope implements Scope { public function apply( Builder $builder, Model $model ) { return $builder->whereMeta( 'is_admin', '1' ); } } ``` ##### Local Scope[​](#local-scope "Direct link to Local Scope") Local Scopes allow you to define a commonly used set of constraints that you may easily re-use throughout your application. For example, you can retrieve all posts that are in a specific category. To add a local scope to the application, prefix a model method with `scope`. Scopes can also accept parameters passed to the scope method. The parameters will be passed down to the scope method after the query builder argument. ```php use Mantle\Database\Model\Post as Base_Post; use Mantle\Database\Query\Post_Query_Builder; class Post extends Base_Post { public function scopeActive( Post_Query_Builder $query ) { return $query->whereMeta( 'active', '1' ); } public function scopeOfType( Post_Query_Builder $query, string $type ) { return $query->whereMeta( 'type', $type ); } } ``` ###### Using a Local Scope[​](#using-a-local-scope "Direct link to Using a Local Scope") To use a local scope, you may call the scope methods with querying the model without the `scope` prefix. Scopes can be chained in the query, too. ```php Posts::active()->get(); Posts::ofType( 'type-to-query' )->get(); ``` #### Querying Models[​](#querying-models "Direct link to Querying Models") The query builder provides a fluent interface to query models. The query builder is a powerful tool to query models in a uniform way. You can call a query builder method on the model itself or call the `query()` method to create a new query builder instance. ```php use App\Models\Post; // Query the model. $results = Post::query()->where( 'post_status', 'publish' )->get(); // Or use the model directly. $query = Post::where( 'post_status', 'publish' ); ``` For more information on querying models, see [Querying Models](/docs/models/query-builder.md). --- ### Database Factory #### Introduction[​](#introduction "Direct link to Introduction") Models can use factories to automatically generate data for your application. They're incredibly useful to get 'real' data in place without the bloat of a database dump. Out of the box, a model does not need a factory defined. It will fallback to the core factory for the respective model type. For example, a post model will fallback to the Mantle default post factory. However, if you want to define a factory for a model, you can do so. See [Defining Custom Factory for a Model](#defining-custom-factory-for-a-model) for more information. #### Creating Models Using Factories[​](#creating-models-using-factories "Direct link to Creating Models Using Factories") You can use the `factory()` method on a model to retrieve the factory for the respective model and start creating models. ```php use App\Models\Post; Post::factory()->create(); // int ``` Factories can also be used statically within a [Mantle Teskit test case](/docs/testing/testkit.md): ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->create_and_get(); // WP_Post. $term_id = static::factory()->tag->create(); // int // ... } } ``` ##### Create a Single Instance[​](#create-a-single-instance "Direct link to Create a Single Instance") Create a model from the factory definition and optionally override it with your own arguments with `create()`. Returns the ID of the created model. * Framework Use * Testkit Use ```php use App\Models\Post; Post::factory()->create(); // int Post::factory()->create( [ 'post_title' => 'My Custom Title', ] ); ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->create_and_get(); // WP_Post. $term_id = static::factory()->tag->create(); // int // ... } } ``` ##### Create Multiple Instances[​](#create-multiple-instances "Direct link to Create Multiple Instances") Create multiple models from the factory definition and optionally override it with your own arguments using the `count()`, `create_many()`, or `create_many_and_get()` methods. It will return an array of IDs of the created models. * Framework Use * Testkit Use ```php use App\Models\Post; Post::factory()->count( 3 )->create(); // int[] Post::factory()->count( 3 )->create( [ 'post_title' => 'My Custom Title', ] ); $instance = Post::factory()->count( 3 )->create_and_get(); // App\Models\Post[] ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post_ids = static::factory()->post->create_many( 3 ); // int[] $term_ids = static::factory()->tag->create_many( 3 ); // int[] // create_many_and_get() can also be used to retrieve multiple objects. $posts = static::factory()->post->create_many_and_get( 3 ); // WP_Post[] $terms = static::factory()->tag->create_many_and_get( 3 ); // WP_Term[] // You may use the fluent count() method to create multiple models, too. $post_ids = static::factory()->post->count( 3 )->create(); // int[] $term_ids = static::factory()->tag->count( 3 )->create(); // int[] // ... } } ``` ##### Create and Get a Model Instance[​](#create-and-get-a-model-instance "Direct link to Create and Get a Model Instance") Create a model from the factory definition and optionally override it with your own arguments using `create_and_get()`. It will return the created model/core object or an array of created models if chained with `count()`: * Framework Use * Testkit Use ```php use App\Models\Post; Post::factory()->create_and_get(); // \App\Models\Post Post::factory()->create_and_get( [ 'post_title' => 'My Custom Title', ] ); ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->create_and_get(); // WP_Post $term = static::factory()->tag->create_and_get(); // WP_Term // count() can be used to create multiple models. $posts = static::factory()->post->count( 3 )->create_and_get(); // WP_Post[] $terms = static::factory()->tag->count( 3 )->create_and_get(); // WP_Term[] // ... } } ``` You can also retrieve a model instance instead of WP\_Post/WP\_Term by chaining the `as_models()` method to the `create_and_get()` method: ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->as_models()->create_and_get(); // Mantle\Database\Model\Post $term = static::factory()->tag->as_models()->create_and_get(); // Mantle\Database\Model\Term // ... } } ``` The return value of `create_and_get()` depends on how the factory is used `create_and_get()` will return either the model instance or the underlying "core object" (WP\_Post/WP\_User/WP\_Term/etc.) depending on the configuration of the factory. When a factory is used via the `Model::factory()` method, `create_and_get()` will return the model instance. When a factory is used via the `static::factory()->post` method in unit tests, `create_and_get()` will return the underlying "core object" (WP\_Post/WP\_User/WP\_Term/etc.). ##### Retrieving or Creating a Model Instance[​](#retrieving-or-creating-a-model-instance "Direct link to Retrieving or Creating a Model Instance") You can use the `first_or_create()` method to retrieve an existing model or create a new one if it doesn't exist. This is useful when you want to ensure that a model exists in the database without having to check for its existence first. * Framework Use * Testkit Use ```php use App\Models\Post; $post = Post::factory()->first_or_create( [ 'post_title' => 'My Custom Title', ], // Additional attributes to set on the post if it is created. [ 'post_content' => 'This is the content of the post.', 'post_status' => 'publish', ] ); ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->first_or_create( [ 'post_title' => 'My Custom Title', ], // Additional attributes to set on the post if it is created. [ 'post_content' => 'This is the content of the post.', 'post_status' => 'publish', ] ); // ... } } ``` #### Defining Custom Factory for a Model[​](#defining-custom-factory-for-a-model "Direct link to Defining Custom Factory for a Model") Out of the box, any model that does not have a factory defined will fallback to the core factory for the respective model type. | Model Type | Core Factory | | ---------- | ------------------------------------------- | | Attachment | Mantle\Database\Factory\Attachment\_Factory | | Site | Mantle\Database\Factory\Blog\_Factory | | Comment | Mantle\Database\Factory\Comment\_Factory | | Network | Mantle\Database\Factory\Network\_Factory | | Post | Mantle\Database\Factory\Post\_Factory | | Term | Mantle\Database\Factory\Term\_Factory | | User | Mantle\Database\Factory\User\_Factory | These factories provide a basic definition for the model. If you want to define a custom factory for a model, you can do so by creating a custom factory for your model. ##### Generating a Factory[​](#generating-a-factory "Direct link to Generating a Factory") You can create a factory by adding a new file in the `database/factory` folder. The expected factory name is: `App\Database\Factory\{Model_Name}_Factory` This can be generated using the `make:factory` command: ```bash bin/mantle make:factory {name} {--model_type=} {--object_name=} ``` ##### Using Custom Model Factory[​](#using-custom-model-factory "Direct link to Using Custom Model Factory") Once a factory is generated, you can modify the `definition()` method of the factory to define the data that should be generated. The `definition()` method should return an array of data that will be used to create the model. ```php namespace App\Database\Factory; use App\Models\Example_Post_Model; /** * Example Post Model Factory * * @extends \Mantle\Database\Factory\Post_Factory<\App\Models\Example_Post_Model> */ class Example_Post_Model_Factory extends \Mantle\Database\Factory\Post_Factory { /** * Model to use when creating objects. * * @var class-string */ protected string $model = Example_Post_Model::class; /** * Define the model's default state. * * @return array */ public function definition(): array { return [ 'post_title' => $this->faker->sentence, 'post_content' => $this->faker->paragraph, 'post_status' => 'publish', 'post_type' => 'post', ]; } } ``` ##### Generating Blocks in Factories[​](#generating-blocks-in-factories "Direct link to Generating Blocks in Factories") All factories have an instance of [Faker](https://fakerphp.github.io/) available to them on the `$this->faker` property. This can be used to generate blocks using the `block()` method. You can optionally include attributes and content. ```php $this->faker->block( 'namespace/block', 'The Content', [ 'exampleAttr' => true, 'another' => false, ] ); ``` Which would produce this: ```html The Content ``` Faker also supports generating paragraph blocks using the `paragraph_block()`/`paragraph_blocks()` methods. ```php $this->faker->paragraph_block( int $sentences = 3 ): string $this->faker->paragraph_blocks( int $count = 3, bool $as_text = true ): string|array ``` ##### Factory States[​](#factory-states "Direct link to Factory States") You can define states for a factory by defining a method on the factory and use the `state()` method to define the state. The `state()` method accepts an array of data to use when creating the model. ```php use Mantle\Database\Factory\Post_Factory; public function my_custom_state(): Post_Factory { return $this->state( [ 'post_title' => 'My Custom Title', ] ); } ``` You can then use the state when creating a model: ```php $example_post = Example_Post_Model::factory()->my_custom_state()->create(); ``` #### Generating Content[​](#generating-content "Direct link to Generating Content") See [Creating Models Using Factories](#creating-models-using-factories) for more information about the `create()`/`create_many()`/`create_and_get()` methods. * Model Factory * Testkit Factory ```php use App\Models\Post; use App\Models\Category; $post_id = Post::factory() ->with_meta( [ 'key' => 'value' ] ) ->create(); $term_id = Category::factory()->tag ->with_meta( [ 'key' => 'value' ] ) ->create(); // ... ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post_id = static::factory()->post ->with_meta( [ 'key' => 'value' ] ) ->create(); // ... $term_id = static::factory()->tag->with_meta( [ 'key' => 'value', ] )->create(); // ... } } ``` ##### Generating Posts/Terms with Meta[​](#generating-poststerms-with-meta "Direct link to Generating Posts/Terms with Meta") Posts and terms can be generated with meta by calling the `with_meta` method on the factory: * Model Factory * Testkit Factory ```php use App\Models\Post; use App\Models\Category; $post_id = Post::factory() ->with_meta( [ 'key' => 'value' ] ) ->create(); $term_id = Category::factory()->tag ->with_meta( [ 'key' => 'value' ] ) ->create(); // ... ``` ```php use App\Models\Post; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post_id = static::factory()->post ->with_meta( [ 'key' => 'value' ] ) ->create(); // ... $term_id = static::factory()->tag->with_meta( [ 'key' => 'value', ] )->create(); // ... } } ``` ##### Generating Posts with Terms[​](#generating-posts-with-terms "Direct link to Generating Posts with Terms") Posts can be generated with terms by calling the `with_terms()` method on the factory: * Model Factory * Testkit Factory ```php use App\Models\Post; use App\Models\Tag; $tag_id = Tag::factory()->create(); $post_id = Post::factory()->with_terms( // Pass in taxonomy => slug pairs [ 'category' => [ 'category_a', 'category_b', ], ], // Pass in a single taxonomy => slug pair. [ 'post_tag' => 'single-tag', ], // Pass in term ID(s). $tag_id, // Or pass in term objects. Tag::factory()->create_and_get(), )->create(); // ... ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $tag_id = static::factory()->tag->create(); $post_id = static::factory()->post->with_terms( // Pass in taxonomy => slug pairs [ 'category' => [ 'category_a', 'category_b', ], ], // Pass in a single taxonomy => slug pair. [ 'post_tag' => 'single-tag', ], // Pass in term ID(s). $tag_id, // Or pass in term objects. static::factory()->tag->create_and_get(), )->create(); // ... } } ``` ##### Generating Terms with Posts[​](#generating-terms-with-posts "Direct link to Generating Terms with Posts") Terms can be generated with posts by calling the `with_posts()` method on the term factory: * Model Factory * Testkit Factory ```php use App\Models\Post; use App\Models\Tag; $post_id = Post::factory()->create(); $term_id = Tag::factory()->with_posts( // Pass in post ID(s). $post_id, // Or pass in post objects. Post::factory()->create_and_get(), )->create(); // ... ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post_id = static::factory()->post->create(); $term_id = static::factory()->tag->with_posts( // Pass in post ID(s). $post_id, // Or pass in post objects. static::factory()->post->create_and_get(), )->create(); // ... } } ``` ##### Generating Post with Thumbnail[​](#generating-post-with-thumbnail "Direct link to Generating Post with Thumbnail") Posts can be generated with a thumbnail by calling the `with_thumbnail` method on the post factory: * Model Factory * Testkit Factory ```php use App\Models\Post; Post::factory()->with_thumbnail()->create_and_get(); ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->with_thumbnail()->create_and_get(); // ... } } ``` The underlying post will have an attachment set as the thumbnail (via the `_thumbnail_id` post meta). The attachment will not have a real image file attached to it for performance. If you'd like a real underlying image file, you can use the `with_real_thumbnail()` method: * Model Factory * Testkit Factory ```php use App\Models\Post; Post::factory()->with_real_thumbnail()->create_and_get(); ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post = static::factory()->post->with_real_thumbnail()->create_and_get(); // ... } } ``` See [Generating an Attachment with a Real Image File](#generating-an-attachment-with-a-real-image-file) for more information. ##### Generating Posts with a Custom Post Type[​](#generating-posts-with-a-custom-post-type "Direct link to Generating Posts with a Custom Post Type") Posts can be generated with a custom post type by calling the `with_post_type()` method on the factory: ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $post_id = static::factory()->post->with_post_type( 'custom_post_type' )->create(); } } ``` ##### Generating an Ordered Set of Posts[​](#generating-an-ordered-set-of-posts "Direct link to Generating an Ordered Set of Posts") Mantle includes a helper to create an ordered set of posts that are evenly spaced out. This can be useful when trying to populate a page with a set of posts and want to verify the order of the posts on the page. * Model Factory * Testkit Factory ```php use App\Models\Post; $post_ids = Post::factory()->create_ordered_set( 10 ); ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_create_ordered_set() { $post_ids = static::factory()->post->create_ordered_set( 10 ); } } ``` The above post IDs are evenly spaced an hour apart starting from a month ago. The start date and the separation can also be adjusted: ```php use Carbon\Carbon; use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_create_ordered_set() { $post_ids = static::factory()->post->create_ordered_set( 10, [], // Start creating the post a year ago. Carbon::now()->subYear(), // Spread them out by a day. DAY_IN_SECONDS, ); } } ``` ##### Generating an Attachment with a Real Image File[​](#generating-an-attachment-with-a-real-image-file "Direct link to Generating an Attachment with a Real Image File") By default, all attachments generated by the factory won't have a "real" file included with it for performance. You can opt to create an attachment with a real image file by calling the `with_image()` method on the attachment factory: * Model Factory * Testkit Factory ```php use App\Models\Attachment; Attachment::factory()->with_image()->create_and_get(); ``` You can also pass a local file to use as the image: ```php use App\Models\Attachment; Attachment::factory()->with_image( __DIR__ . '/image.jpg' )->create_and_get(); ``` ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $attachment_id = static::factory()->attachment->with_image()->create(); // ... } } ``` You can also pass a local file to use as the image: ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $attachment_id = static::factory()->attachment->with_image( __DIR__ . '/image.jpg' )->create(); // ... } } ``` ##### Slashing Content[​](#slashing-content "Direct link to Slashing Content") When generating content, you may want to ensure that the content is slashed correctly. This can prevent errors saving unicode characters in the content of a post. You can use the `slash()` method on the factory to ensure that the content is slashed correctly: ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { public function test_factory() { $block = get_comment_delimited_block_content( 'namespace/block', [ 'class' => 'story story--lg story--float', ], null ); $post_id = static::factory()->post->slash()->create_and_get( [ 'post_content' => $block, ] ); // The block will now have the proper class attribute. Without slash(), // the class attribute would be broken. } } ``` ##### Factory Middleware[​](#factory-middleware "Direct link to Factory Middleware") Mantle includes a middleware system that allows you to hook into the factory process and modify the data before it's saved to the database. This can be useful if you want to modify the data before it's saved or if you want to perform some action after the data is saved. The factory itself uses middleware to assign terms to posts after they're saved, set meta, and more. Middleware can be added to the factory by calling the `with_middleware()` method: ```php use Mantle\Testkit\TestCase as Testkit; class ExampleTest extends Testkit { protected $custom_factory; protected function setUp(): void { parent::setUp(); $this->custom_factory = static::factory()->post->with_middleware( function ( array $args, \Closure $next ) { // Modify the arguments (if needed). // Call the next middleware $result = $next( $args ); // Modify the result (if needed). return $result; } } } ``` --- ### Model Registration Mantle can help you spend less time writing registration code and more time building websites. #### Registering Post Types/Taxonomies[​](#registering-post-typestaxonomies "Direct link to Registering Post Types/Taxonomies") Models can auto-register the object type they represent (a post type for a post model, a taxonomy for a taxonomy model). They can be generated through a `wp-cli` command: ```bash bin/mantle make:model Product --model_type=post --registrable ``` That will generate a model that represents the `product` post type. ```php namespace App\Models; use Mantle\Contracts\Database\Registrable; use Mantle\Database\Model\Post; use Mantle\Database\Model\Registration\Register_Post_Type; class Product extends Post implements Registrable { use Register_Post_Type; /** * Arguments to register the model with. * * @return array */ public static function get_registration_args(): array { return [ 'public' => true, 'rest_base' => static::get_object_name(), 'show_in_rest' => true, 'supports' => [ 'author', 'title', 'editor', 'revisions', 'thumbnail', 'custom-fields', 'excerpt' ], 'taxonomies' => [ 'category', 'post_tag' ], 'labels' => [ // ... ], ]; } } ``` The model should automatically be registered with Mantle. Models are discovered from the `app/models` directory in your application. Mantle will discover your models after updating composer and generating a new model. They can be manually discovered by running the model discovery command: ```bash bin/mantle model:discover ``` ##### Manual Model Registration[​](#manual-model-registration "Direct link to Manual Model Registration") In the event you don't wish to use Mantle's built-in model registration, models can be registered by your application's service provider. This is helpful to allow manual control over the models that are registered on your site. First, disable the automatic registration of models. Generate a new service provider to manage your models: wp mantle make:provider Model\_Service\_Provider Add `App\Providers\Model_Service_Provider::class` to your `config/app.php` file: ```php return [ // Other configuration.... 'providers' => // Providers that are already in place... App\Providers\Model_Service_Provider::class, ], ]; ``` Then update your `app/providers/class-model-service-provider.php` provider to look like this: ```php namespace App\Providers; use Mantle\Support\Service_Provider; use App\Models\Product; class Model_Service_Provider extends Service_Provider { public function boot() { // Boot your models here: Product::boot_if_not_booted(); } public function on_mantle_model_registration() { return []; } } ``` Your model should now be automatically registered on each request. ##### Register REST API Fields[​](#register-rest-api-fields "Direct link to Register REST API Fields") Models can define REST API fields inside of a model easily. Registration should be defined in the model's `boot()` method. To ensure the model's fields are always registered, the model should beadded to the `config/models.php` file under the `register` property. ```php namespace App\Models; use Mantle\Contracts\Database\Registrable_Fields; use Mantle\Database\Model\Post as Base_Post; use Mantle\Database\Model\Registration\Register_Rest_Fields; class Post extends Base_Post implements Registrable_Fields { use Register_Rest_Fields; protected static function boot() { static::register_field( 'field-to-register', function() { return 'value to return'; } ) ->set_update_callback( function( $value ) { // ... } ); } } ``` ##### Register Meta Fields[​](#register-meta-fields "Direct link to Register Meta Fields") Models can define meta values to associate with the model. Similar to registering a model's REST API field, registration should be defined in the model's `boot()` method. To ensure the fields are always registered, the model should be added to the `config/models.php` file under the `register` property. By default, Mantle will pass the `object_subtype` argument for you for the model, registering meta only for the specific object type and object subtype the model represents. In the following example, the meta will be added to the `post` object type in WordPress and the `product` object subtype. ```php namespace App\Models; use Mantle\Contracts\Database\Registrable_Meta; use Mantle\Database\Model\Post; use Mantle\Database\Model\Registration\Register_Meta; class Product extends Post implements Registrable_Meta { use Register_Meta; protected static function boot() { static::register_meta( 'product_id' ); static::register_meta( 'feedback', [ ... ] ); } } ``` #### Bootable Trait Methods[​](#bootable-trait-methods "Direct link to Bootable Trait Methods") To allow for simplicity when writing traits that are shared among a set of models, traits support a `boot` and `initialize` method to allow for automatic registration of the respective trait. The trait name are suffixed with the name of the trait lowercased (for example: `boot_{trait_name}`). ```php trait Example_Trait { public function boot_example_trait() { // Called once per request. } public function initialize_example_trait() { // Called on every model instantiation. } } ``` #### Model Routing[​](#model-routing "Direct link to Model Routing") Models can define post/term singular and archive routes. This will replace the WordPress singular routes for posts and terms with no additional customization needed. Routes inside models can use any model alias or attribute, too. By default, any model that uses the `Register_Post_Type` or `Register_Taxonomy` will have their routes defined for them. The default route format is `/{object_name}/` and `/{object_name}/{slug}/` for the archive and singular route, respectively. The model can define their own route by replacing the `get_route()` or `get_archive_route()` methods. tip For routes that don't use the registration traits the model can still have their routing handled by including `Mantle\Database\Model\Concerns\Custom_Post_Permalink` or `Mantle\Database\Model\Concerns\Custom_Term_Link` traits. ```php use Mantle\Database\Model\Post; use Mantle\Database\Model\Registration\Register_Post_Type; class Product extends Post { use Register_Post_Type; public static function get_route(): ?string { return '/product/{slug}'; } } ``` For more information, see [Model Routing](/docs/basics/requests.md#model-routing) --- ### Model Relationships #### Introduction[​](#introduction "Direct link to Introduction") Data in an application will often have a relationship with other data. In Mantle, data represented as models can have relationships with other models with ease. #### Defining Relationships[​](#defining-relationships "Direct link to Defining Relationships") Relationships are defined as methods on the model. Since, like models themselves, relationships also serve as powerful query builders, defining relationships as methods provides powerful method chaining and querying capabilities. For example, we may chain additional constraints on this posts relationship: ```php $post->sponsors()->whereMeta( 'active', '1' )->get(); ``` Models can exist between posts as well as between posts and term models. Relationships between posts will use an underlying meta query while relationships between posts and terms will use a taxonomy query. ##### Has One/Has Many[​](#has-onehas-many "Direct link to Has One/Has Many") Relationships defined as having one or many will have references to the current model stored on other models. One common example would be a 'post -> sponsor' relationship. The sponsor's ID will be stored on one or many post objects. ```php class Sponsor extends Post { // ... public function posts() { // Uses the 'sponsor_id' meta field on the post to retrieve the sponsor. return $this->has_many( Post::class ); } public function special_post() { return $this->has_one( Post::class )->whereMeta( 'special-meta', '1' ); } } ``` ###### Storing[​](#storing "Direct link to Storing") The relationship can automatically setup the proper meta values to define a relationship. ```php $sponsor->save( $post ); $sponsor->remove( $post ); ``` ##### Belongs To / Belongs To Many[​](#belongs-to--belongs-to-many "Direct link to Belongs To / Belongs To Many") Relationships can be define as belong to another model will have the reference stored on the other model. In the example of a `post -> sponsor` relationship, the sponsor's ID will be stored as meta on the post object. ```php class Post extends Base_Post { // ... public function sponsor() { // Uses the 'sponsor_id' meta field on the post to retrieve the sponsor. return $this->belongs_to( Sponsor::class ); } public function tags() { return $this->belongs_to( Sponsor::class ); } } ``` ###### Storing[​](#storing-1 "Direct link to Storing") The relationship can automatically setup the proper meta values on the model to define a relationship. ```php $sponsor->associate( $post ); $sponsor->dissociate( $post ); ``` ##### Post-to-Post Relationships[​](#post-to-post-relationships "Direct link to Post-to-Post Relationships") Posts can define relationships between other posts using meta keys by default. However, this can result in poor query performance when querying against the relationship. Mantle supports defining relationships between posts using a internal taxonomy. This will result in better query performance when loading the post's relationships. ```php class Sponsor extends Post { public function posts() { return $this->has_many( Post::class )->uses_terms(); } } ``` On the flip side, the belongs-to relationship needs to use terms as well. ```php class Post extends Base_Post { public function sponsor() { return $this->belongs_to( Sponsor::class )->uses_terms(); } } ``` ##### Post-to-Term Relationships[​](#post-to-term-relationships "Direct link to Post-to-Term Relationships") Post-to-term relationships should always use a Has One/Has Many in both directions since post and terms in WordPress are bidirectional. Attempting to define a relation between a post and a term using Belongs To will result in an error being thrown. #### Querying Relationships[​](#querying-relationships "Direct link to Querying Relationships") Relationships can be queried by using the method on the model (which uses a [query builder](/docs/models/query-builder.md) to construct the query). ```php $post->sponsors()->get(); $post->sponsors()->first(); ``` Relationships can also be access as magic properties on the model instance. ```php echo $post->sponsor->title; foreach ( $post->sponsors as $sponsor ) { echo $sponsor->title; } ``` #### Eager Loading Relationships[​](#eager-loading-relationships "Direct link to Eager Loading Relationships") When using relationships as model properties, related models are "lazy loaded" by default. This means that the relationship data is not loaded until the first time you access the property. Mantle supports eager loading relationships to load all model relationships at the time of the initial query for the parent model. This can prevent the "N + 1" query problem. Here's an example of a problem eager loading can prevent: 1. A page includes a list of blog posts. 2. Each blog post has a sponsor relationship used. 3. During the loop, each blog post will cause an additional query (the "N + 1" problem) to retrieve the post's sponsor. Eager Loading the relationship can prevent this by loading all sponsors for the blog posts in a collection at once. Here is an example model: ```php class Blog_Post extends Post { public function sponsor() { return $this->has_one( Sponsor::class ); } } ``` Now, let's retrieve all the blog posts and the sponsors: ```php $posts = Blog_Post::with( 'sponsor' )->get(); foreach ( $posts as $post ) { echo $post->title; echo $post->sponsor->title; } ``` This loop will execute two queries: the initial query to retrieve the blog posts and another query to retrieve the sponsors. Without eager loading, this would perform "N + 1" queries on the page. Eager Loading is by default an opt-in feature of Mantle models for performance. A model can include an eager-loaded relationship by default by setting the with property on the model. ```php class Blog_Post extends Post { /** * The relations to eager load on every query. * * @var string[] */ protected $with = [ 'sponsor' ]; } ``` --- ### Query Builder Models support a fluent query builder that will return an easy-to-use Collection of models. tip The Mantle Framework is not required to use the query builder. You can require the package in your application with Composer and use the models and query builder in your application. ```bash composer require mantle-framework/database ``` #### Retrieving Models[​](#retrieving-models "Direct link to Retrieving Models") Once a model is defined you are ready to start retrieving data. Each model acts as a powerful query builder that allows you to fluently query the underlying data that each model represents. ##### Querying a Model by ID[​](#querying-a-model-by-id "Direct link to Querying a Model by ID") If you know a model's primary key, you can retrieve it using the `find()` method. ```php use App\Models\Post; $post = Post::find( 1 ); ``` ##### Queuing Collections of Models[​](#queuing-collections-of-models "Direct link to Queuing Collections of Models") The `all()` method will return all the results for a model. ```php use App\Models\Post; $posts = Post::all(); foreach ( $posts as $post ) { echo $post->title; } ``` tip For post models, the `all` method will retrieve only published posts by default. You can easily include all post statuses by calling `anyStatus()` on the model. ```php use App\Models\Post; Post::where( ... )->anyStatus()->all(); ``` Retrieving the results of the current query can be done by using the `get()` method. ```php use App\Models\Post; $posts = Post::where( 'post_status', 'publish' )->get(); ``` You can use the `found_rows()` method to retrieve the total number of results from the collection of models returned by the query. ```php use App\Models\Post; $posts = Post::where( 'post_status', 'publish' )->get(); $total = $posts->found_rows(); ``` ##### Querying a Single Model[​](#querying-a-single-model "Direct link to Querying a Single Model") You can also use the `first()` method to retrieve just a single model from either the model or the model's query builder. ```php use App\Models\Post; $post = Post::first(); // Retrieve the first post with the title "example". $post = Post::where( 'title', 'example' )->first(); ``` ##### Counting the Number of Results[​](#counting-the-number-of-results "Direct link to Counting the Number of Results") You can count the number of results for a query by using the `count()` method. ```php use App\Models\Post; $count = Post::where( 'post_status', 'publish' )->count(); ``` #### Querying Models[​](#querying-models "Direct link to Querying Models") Queries can be started by calling the `query()` method on the model or using any query method directly on the model statically. ```php use App\Models\Post; // Using the static magic method. Post::where( 'post_status', 'publish' )->get(); // Using the query method. Post::query()->where( 'post_status', 'publish' )->get(); ``` ##### Querying Model Fields[​](#querying-model-fields "Direct link to Querying Model Fields") Model fields can be queried against using the `where()` method or using magic-methods in the format of `where{Field}()` to fluently build queries. ```php Example_Post::where( 'slug', 'slug-to-find' )->first(); Example_Post::whereSlug( 'slug-to-find' )->first(); ``` You can use the field name or any [field alias](/docs/models.md#field-aliases) to query against. Mantle will automatically convert the field name to the correct argument for the underlying `WP_Query` or `WP_Tax_Query` class. ###### Chaining Queries[​](#chaining-queries "Direct link to Chaining Queries") Queries can be chained together to build more complex queries. ```php Example_Post::where( 'slug', 'slug-to-find' ) ->where( 'post_status', 'publish' ) ->first(); ``` ###### Querying Model in a List of IDs[​](#querying-model-in-a-list-of-ids "Direct link to Querying Model in a List of IDs") You can also use `whereIn()` and `whereNotIn()` to query against a list of values to retrieve models who are in or not in the list of IDs. ```php // Posts in a list of IDs. Example_Post::whereIn( 'id', [ 1, 2, 3 ] )->get(); // Posts not a list of IDs. Example_Post::whereNotIn( 'id', [ 1, 2, 3 ] )->get(); ``` ##### Querying Posts with Terms[​](#querying-posts-with-terms "Direct link to Querying Posts with Terms") Post queries can be built to query against terms. Only post models support querying against terms. ```php // Get the first 10 posts in the 'term-slug' tag. Example_Post::whereTerm( 'term-slug', 'post_tag' ) ->take( 10 ) ->get(); // Get the first 10 posts in the 'term-slug' or 'other-tag' tags. Example_Post::whereTerm( 'term-slug', 'post_tag' ) ->orWhereTerm( 'other-tag', 'post_tag' ) ->take( 10 ) ->get(); // Get the first 10 posts in the taxonomy term controlled by `Example_Term`. $term = Example_Term::first(); Example_Post::whereTerm( $term )->take( 10 )->get(); ``` ##### Querying with Meta[​](#querying-with-meta "Direct link to Querying with Meta") Normal concerns for querying against a model's meta still should be observed. The `Post` and `Term` models support querying with meta. ```php // Instance of Example_Post if found. Example_Post::whereMeta( 'meta-key', 'meta-value' )->first(); // Multiple meta keys to match. Example_Post::whereMeta( 'meta-key', 'meta-value' ) ->andWhereMeta( 'another-meta-key', 'another-value' ) ->first(); Example_Post::whereMeta( 'meta-key', 'meta-value' ) ->orWhereMeta( 'another-meta-key', 'another-value' ) ->first(); ``` ##### Querying By Date[​](#querying-by-date "Direct link to Querying By Date") Mantle provides a few methods to query against dates to circumvent the limitations of `WP_Query`. All date arguments support a date string, unix timestamp, Carbon instance, or DateTime/DateTimeInterface instance. All comparison operators support `=`, `!=`, `>`, `>=`, `<`, `<=`. Where possible, the query will be converted to a `date_query` argument for `WP_Query` to use. If the query cannot be converted to a `date_query` argument, Mantle will use a raw SQL query to query against the database column directly. This applies when trying to query [a specific date](https://core.trac.wordpress.org/ticket/59351) which WordPress does not support. The date methods are only available on post models. ###### whereDate[​](#wheredate "Direct link to whereDate") Find posts where the date matches a specific value. Supports a date string, unix timestamp, Carbon instance, or DateTime instance. The default column to query against is `post_date`. ```php use App\Models\Post; $posts = Post::query() ->whereDate( '2021-01-01', '>' ) ->get(); $posts = Post::query() ->whereDate( 1612137600, '>' ) ->get(); $posts = Post::query() ->whereDate( new DateTime( '2021-01-01' ), '>', 'post_modified' ) ->get(); ``` ###### whereUtcDate[​](#whereutcdate "Direct link to whereUtcDate") Find posts where the `post_date_gmt` matches a specific value. Supports a date string, unix timestamp, Carbon instance, or DateTime instance. ```php use App\Models\Post; $posts = Post::query() ->whereUtcDate( '2021-01-01' ) ->get(); $posts = Post::query() ->whereUtcDate( 1612137600, '>' ) ->get(); ``` ###### whereModifiedDate[​](#wheremodifieddate "Direct link to whereModifiedDate") Find posts where the `post_modified` matches a specific value. ```php use App\Models\Post; $posts = Post::query() ->whereModifiedDate( '2021-01-01', '>' ) ->get(); ``` ###### whereModifiedUtcDate[​](#wheremodifiedutcdate "Direct link to whereModifiedUtcDate") Find posts where the `post_modified_gmt` matches a specific value. ```php use App\Models\Post; $posts = Post::query() ->whereModifiedUtcDate( '2021-01-01', '>' ) ->get(); ``` ###### olderThan[​](#olderthan "Direct link to olderThan") Find posts where the date is older than a specific value. ```php use App\Models\Post; $posts = Post::query() ->olderThan( '2021-01-01' ) ->get(); ``` ###### olderThanOrEqualTo[​](#olderthanorequalto "Direct link to olderThanOrEqualTo") Find posts where the date is older than or equal to a specific value. ```php use App\Models\Post; $posts = Post::query() ->olderThanOrEqualTo( '2021-01-01' ) ->get(); ``` ###### newerThan[​](#newerthan "Direct link to newerThan") Find posts where the date is newer than a specific value. ```php use App\Models\Post; $posts = Post::query() ->newerThan( '2021-01-01' ) ->get(); ``` ###### newerThanOrEqualTo[​](#newerthanorequalto "Direct link to newerThanOrEqualTo") Find posts where the date is newer than or equal to a specific value. ```php use App\Models\Post; $posts = Post::query() ->newerThanOrEqualTo( '2021-01-01' ) ->get(); ``` ##### Limit Results[​](#limit-results "Direct link to Limit Results") You can limit the number of results returned by using the `take()` method. ```php // Get the first 10 posts. Example_Post::where( 'title', 'example' )->take( 10 )->get(); ``` ##### Pages[​](#pages "Direct link to Pages") You can paginate results by using the `page()` method. ```php // Get the second page of results. Example_Post::where( 'title', 'example' )->page( 2 )->get(); ``` For more information on advanced pagination, see the [Pagination](#pagination) section. ##### Ordering Results[​](#ordering-results "Direct link to Ordering Results") Query results can be ordered by using the `orderBy()`/`order_by()` methods. ```php Example_Post::query()->orderBy( 'name', 'asc' )->get(); ``` You can also order by the value passed to `whereIn()`: ```php Example_Post::query() ->whereIn( 'id', [ 1, 2, 3 ] ) ->orderByWhereIn() ->get(); ``` You can pass multiple fields and directions to the `orderBy()` method by calling it multiple times. ```php Example_Post::query() ->orderBy( 'name', 'asc' ) ->orderBy( 'date', 'desc' ) ->get(); ``` To remove all ordering from a query, you can use the `removeOrder()` method. ```php Example_Post::query()->removeOrder()->get(); ``` ##### Conditional Clauses[​](#conditional-clauses "Direct link to Conditional Clauses") Sometimes you may want certain clauses to be applied only if a condition is met. You can use the `when()` method to conditionally add clauses to a query. ```php use App\Models\Post; use Mantle\Database\Query\Post_Query_Builder; Post::query() ->when( $request->has( 'name' ), fn ( Post_Query_Builder $query ) => $query->where( 'name', $request->get( 'name' ) ), ) ->get(); ``` The closure will only be executed if the condition is met. You can pass another callback to the third argument of the `when()` method to add a clause when the condition is not met. ```php use App\Models\Post; use Mantle\Database\Query\Post_Query_Builder; Post::query() ->when( $request->has( 'name' ), // Applied when the condition is met. fn ( Post_Query_Builder $query ) => $query->where( 'name', $request->get( 'name' ) ), // Applied when the condition is not met. fn ( Post_Query_Builder $query ) => $query->where( 'name', 'default-name' ), ) ->get(); ``` ##### Chunking Results[​](#chunking-results "Direct link to Chunking Results") If you need to work with thousands of models, you can use the `chunk()` method to process the results in chunks. ```php use App\Models\Post; // Chunk the results in groups of 100. Post::chunk( 100, function ( $posts ) { foreach ( $posts as $post ) { // Do something with the post. } } ); ``` You can also use the `chunk_by_id()` method to chunk results by the model's ID. This is useful if you need to process results in batches and potentially delete/modify them in a way that can break the sorting of the query. ```php use App\Models\Post; // Chunk the results in groups of 100. Post::chunk_by_id( 100, function ( $posts ) { foreach ( $posts as $post ) { // Do something with the post. } } ); ``` ###### Each/Each By ID[​](#eacheach-by-id "Direct link to Each/Each By ID") If you need to process each model in a query, you can use the `each()`/`each_by_id()` methods. These methods will process each model in the query performantly without loading all of the models into memory. `each_by_id()` is useful if you need to process results in batches and potentially delete/modify them in a way that can break the sorting of the query. ```php use App\Models\Post; // Process each post using normal pagination chunking. Post::where( 'status', 'publish' )->each( function ( Post $post ) { // Do something with the post. } ); // Process each post using ID-based pagination chunking. Post::where( 'status', 'publish' )->each_by_id( function ( Post $post ) { // Do something with the post. } ); ``` ###### Map/Map By ID[​](#mapmap-by-id "Direct link to Map/Map By ID") If you need to map the results of a query, you can use the `map()`/`map_by_id()` methods. These methods will map the results of the query performantly without loading all of the models into memory. `map_by_id()` is useful if you need to process results in batches and potentially delete/modify them in a way that can break the sorting of the query. ```php use App\Models\Post; // Map the results using normal pagination chunking. $posts = Post::where( 'status', 'publish' )->map( function ( Post $post ) { // Perform some additional processing here. return [ $post->id => $post->title ]; } ); // Map the results using ID-based pagination chunking. $posts = Post::where( 'status', 'publish' )->map_by_id( function ( Post $post ) { // Perform some additional processing here. return [ $post->id => $post->title ]; } ); ``` ##### Debugging Queries[​](#debugging-queries "Direct link to Debugging Queries") The query arguments that are passed to the underlying `WP_Query`/`WP_Tax_Query`/etc. can be dumped by using the `dump()`/`dd()` methods. ```php use App\Models\Post; // Dump the query arguments. Post::query()->where( 'name', 'example' )->dump()->get(); // Dump the query arguments and end the script. Post::query()->where( 'name', 'example' )->dd(); ``` You can dump the raw SQL query by using the `dump_sql()`/`dd_sql()` methods. ```php use App\Models\Post; // Dump the raw SQL query. Post::query()->where( 'name', 'example' )->dump_sql(); // Dump the raw SQL query and end the script. Post::query()->where( 'name', 'example' )->dd_sql(); ``` ##### Advanced: Querying Against Database Columns[​](#advanced-querying-against-database-columns "Direct link to Advanced: Querying Against Database Columns") You can query against the database columns directly by using the `whereRaw()`/`orWhereRaw()` methods. This allows you to write raw SQL queries to query against the database columns to circumvent the limitations of `WP_Query` and `WP_Tax_Query`. ```php use App\Models\Post; // Get the first 10 posts where the comment count is greater than 10. $posts = Post::query()->whereRaw( 'comment_count', '>', 10 )->get(); // Get a post by ID using a raw SQL query. $post = Post::query()->whereRaw( 'ID', 1 )->first(); // Partial match the post title using a raw SQL query. $posts = Post::query() ->whereRaw( 'post_title', 'LIKE', 'example prefix:%' ) ->get(); // Attach multiple conditions together. $posts = Post::query() ->whereRaw( 'comment_count', '>', 10 ) ->orWhereRaw( 'comment_count', '<', 5 ) ->get(); ``` These methods are useful for more complex queries that cannot be expressed using `WP_Query` or `WP_Tax_Query` via the fluent query builder. For example, Mantle uses this internally to chunk models by ID (which is not supported by `WP_Query`). The variables passed to `whereRaw()`/`orWhereRaw()` are automatically escaped with `$wpdb->prepare()` to prevent SQL injection. ##### Advanced: Modifying Query Clauses[​](#advanced-modifying-query-clauses "Direct link to Advanced: Modifying Query Clauses") Apart of the above raw SQL queries, you can also modify the query clauses directly by using the `add_clause()` method on post/term queries. This allows you to modify post/term query clauses that are exposed via the `posts_clauses`/`terms_clauses` filters. The callback passed to `add_clause()` will receive the current query clauses and should return the modified query clauses. See the relevant WordPress core documentation for the `posts_clauses` and `terms_clauses` filters for more information: * [`posts_clauses`](https://developer.wordpress.org/reference/hooks/posts_clauses/) * [`terms_clauses`](https://developer.wordpress.org/reference/hooks/terms_clauses/) ```php use App\Models\Post; // Add a custom clause to the post query. $posts = Post::query()->add_clause( function ( array $clauses ) { $clauses['where'] .= ' AND post_date > "2021-01-01"'; return $clauses; } )->get(); ``` Mantle recommends to use the [`whereRaw()`/`orWhereRaw()`](#advanced-querying-against-database-columns) methods over `add_clause()` as it is more expressive and easier to understand. The `add_clause()` method is useful to make it simpler than WordPress core makes it to quickly modify the query clause directly. #### Retrieving or Creating Models[​](#retrieving-or-creating-models "Direct link to Retrieving or Creating Models") Models can be retrieved or created if they do not exist using the following helper methods: ##### first\_or\_new[​](#first_or_new "Direct link to first_or_new") The `first_or_new()` method will retrieve the first model matching the query or create a new instance of the model if no matching model exists. It will not save the model. The second argument is an array of attributes to set on the model if it is created. ```php use App\Models\Post; // Retrieve the first post with the title "example" or create a new post. $post = Post::first_or_new( [ 'title' => 'example', ], [ 'content' => 'This is an example post.', ] ); ``` ##### first\_or\_create[​](#first_or_create "Direct link to first_or_create") Similar to `first_or_new()`, the `first_or_create()` method will retrieve the first model matching the query or create a new instance of the model if no matching model exists. It will save the model. The second argument is an array of attributes to set on the model if it is created. ```php use App\Models\Post; // Retrieve the first post with the title "example" or create a new post. $post = Post::first_or_create( [ 'title' => 'example', ], [ 'content' => 'This is an example post.', ] ); ``` #### Model Not Found Exceptions[​](#model-not-found-exceptions "Direct link to Model Not Found Exceptions") The `first_or_fail()` method will retrieve the first model matching the query or throw a `Mantle\Database\Model\Model_Not_Found_Exception` if no matching model exists. ```php use App\Models\Post; // Retrieve the first post with the title "example" or throw an exception. $post = Post::where( 'title', 'example' )->first_or_fail(); ``` The `find_or_fail()` method will retrieve the model by its primary key or throw a `Mantle\Database\Model\Model_Not_Found_Exception` if no matching model exists. ```php use App\Models\Post; // Retrieve the post with the ID of 1 or throw an exception. $post = Post::find_or_fail( 1 ); ``` ##### update\_or\_create[​](#update_or_create "Direct link to update_or_create") The `update_or_create()` method will retrieve the first model matching the query, create it if it does not exist, or update it if it exists. The second argument is an array of attributes to set on the model if it is created. ```php use App\Models\Post; // Retrieve the first post with the title "example" or create a new post. $post = Post::update_or_create( [ 'title' => 'example', ], [ 'content' => 'This is an example post.', ] ); ``` #### Querying Multiple Models[​](#querying-multiple-models "Direct link to Querying Multiple Models") Multiple models of the same type (posts/terms/etc.) can be queried together. There are some limitations and features that cannot be used including query scopes. ```php use Mantle\Database\Query\Post_Query_Builder; use App\Models\Post; use App\Models\Another_Post; Post_Query_Builder::create( [ Post::class, Another_Post::class ] ) ->whereMeta( 'shared-meta', 'meta-value' ) ->get(); ``` #### Pagination[​](#pagination "Direct link to Pagination") All models support pagination of results to make traversal of large sets of data easy. The paginators will display links to the next and previous pages as well as other pages that can be styled by your application. ##### Length Aware Pagination[​](#length-aware-pagination "Direct link to Length Aware Pagination") By default the `paginate()` method will use the Length Aware Paginator which will calculate the total number of pages in a result set. This is what you probably expect from a paginator: previous / next links as well as links to a few pages before and after the current one. ```php use App\Models\Posts; $posts = Posts::paginate( 20 ); ``` ##### Basic Pagination[​](#basic-pagination "Direct link to Basic Pagination") 'Basic' pagination is purely a next / previous link relative to the current page. It also sets `'no_found_rows' => true` on the query to help performance for very large data sets. ```php use App\Model\Posts; $posts = Posts::simple_paginate( 20 ); ``` ##### Displaying Paginator Results[​](#displaying-paginator-results "Direct link to Displaying Paginator Results") The paginator instances both support iteration over it to allow you to easily loop through and display the current page's results. ```php {{ $posts->links() }} ``` ##### Customizing the Paginator Links[​](#customizing-the-paginator-links "Direct link to Customizing the Paginator Links") By default the paginator will use query strings to paginate and determine the current URL. ```text /blog/ /blog/?page=2 ... /blog/?page=100 ``` The paginator can also be set to use path pages to paginate. For example, a paginated URL would look like `/blog/page/2/`. ```php {{ $posts->use_path()->links() }} ``` ##### Append Query Parameters to Paginator Links[​](#append-query-parameters-to-paginator-links "Direct link to Append Query Parameters to Paginator Links") Arbitrary query parameters can be append to paginator links. ```php {{ $posts->append( [ 'key' => 'value' ] )->links() }} ``` The current GET query parameters can also be automatically included on the links as well. ```php {{ $posts->with_query_string()->links() }} ``` ##### Customizing the Paginator Path[​](#customizing-the-paginator-path "Direct link to Customizing the Paginator Path") The paginator supports setting a custom base path for paginated results. By default the paginator will use the current path (stripping `/page/n/` if it includes it). ```php {{ $posts->path( '/blog/' )->links() }} ``` ##### Converting the Paginator to JSON[​](#converting-the-paginator-to-json "Direct link to Converting the Paginator to JSON") The paginator supports being returned directly as a route's response. ```php Route::get( '/posts', function() { return App\Posts::paginate( 20 ); } ); ``` The paginator will return the results in a JSON format. ```php { "current_page": 1, "data": [ ... ], "first_page_url": "\/path", "next_url": "\/path?page=2", "path": "\/path", "previous_url": null } ``` --- ### Database Seeding #### Introduction[​](#introduction "Direct link to Introduction") Mantle includes a seeding and factory framework to allow you to quickly get test data in your application. All seeders live in the `database/seeds` folder of the application. The application includes a `Database_Seeder` class by default which can be used to call additional seeders. #### Writing a Seeder[​](#writing-a-seeder "Direct link to Writing a Seeder") ```bash wp mantle make:seeder Class_Name ``` #### Seed Data Using Factories[​](#seed-data-using-factories "Direct link to Seed Data Using Factories") You can use a factory to generate many instances of a model. Once you have defined a factory for a model you can use the factory to quickly pump out data for your application. ```php /** * Run the database seeds. */ public function run() { \App\Models\User::factory()->create_many( 50 ); } ``` #### Calling Seeders[​](#calling-seeders "Direct link to Calling Seeders") By default the seeder command will invoke the `Database_Seeder` class in the application. You can specify a different seeder to run by specifying the `--class` flag. ```bash wp mantle db:seed --class ``` #### Calling Additional Seeders[​](#calling-additional-seeders "Direct link to Calling Additional Seeders") Within a seeder you can use the `call` method to invoke additional seeders. Using the `call` method allows you to break up your database seeding into multiple files so that no single seeder class becomes overwhelmingly large. Pass the name of the seeder class you wish to run. ```php /** * Run the database seeds. */ public function run() { $this->call( [ User_Seeder::class, Post_Seeder::class, Comment_Seeder::class, ] ); } ``` --- ### Serialization Often times a Model will need to be converted to arrays or JSON for use in a API endpoint. Mantle includes convenient methods for making these conversions and controlling which attributes remain hidden/visible in your serializations. #### Serializing Models[​](#serializing-models "Direct link to Serializing Models") Models implement the `Arrayable` contract and include a convenient `to_array()` method to convert a model to an array. This method is recursive, so all attributes and all relations (including the relations of relations) will be converted to arrays: ```php $post = App\Models\Post::first(); return $post->to_array(); ``` A collection of models will also properly serialize all models to array. ```php $posts = App\Models\Post::all(); return $posts->to_array(); ``` #### Hiding Attributes from Serialization[​](#hiding-attributes-from-serialization "Direct link to Hiding Attributes from Serialization") Sometimes you may need to hide attributes from serialization to prevent public display. Model attributes can be hidden from serialization using the `$hidden` property on your model, or using the `set_hidden()` or `make_hidden_if()` methods on the model. ```php namespace App\Models; use Mantle\Database\Model\Post; class Product extends Post { /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'post_title' ]; } ``` Alternatively, you may use the `visible` property to define an allow list of attributes that should be included in your model's array and JSON representation. In addition to the `$visible` property, you can also use `set_visible()` or `make_visible_if()` methods on the model, too. Once an attribute is set visible, all other attributes will be hidden: ```php namespace App\Models; use Mantle\Database\Model\Post; class Product extends Post { /** * The attributes that should be visible for arrays. * * @var array */ protected $visible = [ 'post_title', 'post_content ]; } ``` ##### Temporarily Setting Attribute Visibility[​](#temporarily-setting-attribute-visibility "Direct link to Temporarily Setting Attribute Visibility") Attributes on a model can use the additional methods on the model to set attribute visibility. ```php $post->make_visible( 'attribute' )->to_array(): $post->make_hidden( 'title' )->to_array(): ``` #### Appending Values to Serialization[​](#appending-values-to-serialization "Direct link to Appending Values to Serialization") When converting to an array or JSON, you may need to append attributes that do not have a corresponding column in your database. This can be accomplished by defining a custom accessor for the value and including it in the `appends` property on your model: ```php use Mantle\Database\Model\Post; class Author extends Post { protected $appends = [ 'avatar' ]; public function get_avatar_attribute(): string { return "https://example.org/{$this->id}.jpg"; } } ``` In the `Author` model, an `avatar` attribute will be included in the serialized response. An attribute can also be appended at run-time: ```php $author->append( 'avatar' )->to_array(); $author->set_appends( 'avatar' )->to_array(); ``` --- ### Testing Mantle provides a powerful testing framework to help you write and run tests for your WordPress applications. It is designed to be simple to use and IDE-friendly. For use with existing projects using core's testing framework, [Mantle Testkit](/docs/testing/testkit.md) can be used to replace the core test suite with Mantle's testing framework. #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides a PHPUnit test framework to make it easier to test your code with WordPress. It is focused on making testing your application faster and easier, allowing unit testing to become top of mind when building your site. Mantle includes many convenient helpers to allow you to expressively test your applications. ```php use App\Tests\TestCase; class ExampleTest extends TestCase { public function test_example(): void { $post = static::factory()->post->create_and_get(); $this->get( get_permalink( $post ) ) ->assertOk() ->assertQueriedObject( $post ) ->assertSee( $post->post_title ); } } ``` With Mantle, your application's tests live in your `tests` directory. Tests should extend from the `App\Tests\Test_Case` test case, which include booting and the use of your Mantle application inside of your test case. By default, your application's `tests` directory contains two directories: `Feature` and `Unit`. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Tests within your "Unit" test directory do not boot your Mantle application and therefore are unable to access your application's database or other framework services. The tests within "Unit" extend from the base PHPUnit test case and cannot use the rest of the testing framework. Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint. **Generally, most of your tests should be feature tests. These types of tests provide the most confidence that your system as a whole is functioning as intended.** Interested in using the testing framework outside of a Mantle application? [Mantle Testkit](/docs/testing/testkit.md) is a standalone package that can be used to run Mantle's testing framework in any WordPress project. [Learn more about Mantle Testkit](/docs/testing/testkit.md). #### Supported PHPUnit Versions[​](#supported-phpunit-versions "Direct link to Supported PHPUnit Versions") Mantle supports PHPUnit 10-12 with 12 being the default version for new projects. Tests should be written using PSR-4 file/class naming conventions to ensure compatibility with PHPUnit 10 and later (e.g. `tests/Feature/ExampleTest.php`). Mantle < 1.14 supported PHPUnit 9.x Mantle versions prior to 1.14 supported PHPUnit 9.x. If you are using PHPUnit 9.x, you will need to use Mantle 1.13.x or earlier. When you are ready, you can [migrate to PHPUnit 10+](/docs/testing/migration-phpunit-9.md). #### Creating Tests[​](#creating-tests "Direct link to Creating Tests") To create a new test case, use the `make:test` command. By default, the tests will be placed in the `tests` directory. *Note: These commands only work for Mantle applications.* ```bash bin/mantle make:test Namespace\Test_Name> wp mantle make:test ``` #### Running Tests[​](#running-tests "Direct link to Running Tests") After installing `alleyinteractive/mantle-framework` or `mantle-framework/testkit`, you can begin writing and running tests for your WordPress application. Unit tests can be run directly or via Composer: ```bash ./vendor/bin/phpunit ``` Interested in writing tests using Pest? Mantle's testing framework is fully compatible with Pest. You can use Pest to write your tests if you prefer its syntax and features. [Learn more about using Pest with Mantle](/docs/testing/pest.md). ##### Running Tests in Parallel[​](#running-tests-in-parallel "Direct link to Running Tests in Parallel") Mantle's testing framework supports running tests in parallel using [`paratest`](https://github.com/brainmaestro/paratest). To get started, install `brianium/paratest` as a development dependency: ```bash composer require --dev brianium/paratest ``` `paratest` is a drop-in replacement for PHPUnit that runs your tests in parallel. To switch to using `paratest`, replace `phpunit` with `paratest` when running your tests: ```bash ./vendor/bin/paratest ``` When running in parallel, each test process will use its own database prefix to isolate the processes from each other. By default, the prefix will be `wptests_` followed by an incrementing number (e.g. `wptests_1`, `wptests_2`, etc.). For the most part, running tests in parallel should "just work." However, there are some cases where you may need to make adjustments to your tests to ensure they work correctly when run in parallel. For example, if your tests rely on global state (e.g. global variables, singletons, etc.), you may need to refactor your tests to avoid that global state or reset it between tests. #### Background[​](#background "Direct link to Background") ##### Why This Instead of WordPress Core's Test Suite?[​](#why-this-instead-of-wordpress-cores-test-suite "Direct link to Why This Instead of WordPress Core's Test Suite?") We hope nobody interprets Mantle's Test Framework as a slight against WordPress Core's test suite. We ❤️ WordPress Core's test suite and Mantle's Test Framework is unequivocally a derivative work of it. WordPress Core's test suite ("wordpress-develop", if you will) is a wonderful test suite for testing WordPress itself. We, and many others in the WordPress community, have been repurposing it for years to help us run plugin and theme tests. That's worked fine, but it's not optimal. Mantle's Test Framework tries to incorporate the best parts of WordPress Core's test suite, but remove the unnecessary bits. Without having to worry about older versions of PHP, that also allows Mantle's Test Framework to use the latest versions of PHPUnit itself. ##### Drop-in Support for Core Test Suite[​](#drop-in-support-for-core-test-suite "Direct link to Drop-in Support for Core Test Suite") The Mantle Test Framework includes support for WordPress core's test suite methods, including `go_to()` and `$this->factory()` among others. Projects are able to switch to the Mantle Test Framework without needing to rewrite any existing unit tests. See the [Mantle Test Kit](/docs/testing/testkit.md) for more information. ##### Future Plans[​](#future-plans "Direct link to Future Plans") Mantle's Test Framework is actively being developed. Our future plans are to continue making the testing framework the best possible experience for testing WordPress projects. Testing is a critical part of building high-quality software, and we want to make that as easy as possible for WordPress developers. #### Using the Testing Framework[​](#using-the-testing-framework "Direct link to Using the Testing Framework") The testing framework is flexible enough to support running tests in a variety of environments. The most common use case is running tests in an existing WordPress project. For example, you could run tests within a plugin that is located within a larger WordPress project. This would fall under the [Running Tests Within a WordPress Project](#running-tests-within-a-wordpress-project) guide for using an existing WordPress project to run tests against. The framework also supports running tests within an isolated project. For example, a standalone plugin/theme that is not located inside a WordPress project. This would fall under the [Running Tests in a Standalone Project](#running-tests-in-a-standalone-project) guide for using an isolated project to run tests against. Mantle's Test Framework provides a special bootstrapper and installer for WordPress. It is common in WordPress to use a *separate* WordPress codebase when running unit tests. In Mantle, you use the same codebase and a separate database. As long as your test suite isn't writing to any files, a singular codebase is a preferable setup, especially if you want to use XDebug to step through your test or want to rely on your IDE to discover testing framework methods. ##### Running Tests Within a WordPress Project[​](#running-tests-within-a-wordpress-project "Direct link to Running Tests Within a WordPress Project") When running tests within a WordPress project, Mantle will use the existing WordPress installation to run tests against. This is the most common use case for Mantle's Test Framework. While the codebase will be used, the database will not be. Mantle will attempt to [use a default configuration](https://github.com/alleyinteractive/mantle-ci/blob/main/wp-tests-config-sample.php) to connect to locally. The default configuration will install WordPress using a `localhost` database named `wordpress_unit_tests` with the username/password pair of `root/root`. This can be overridden by defining your own `wp-tests-config.php` file in the root of your WordPress project. Mantle can generate a `wp-tests-config.php` file for you You can generate your own config file by running `bin/mantle test-config`. ##### Running Tests in a Standalone Project[​](#running-tests-in-a-standalone-project "Direct link to Running Tests in a Standalone Project") A standalone project that isn't located within an existing WordPress project can be used to run tests against. Mantle will automatically install WordPress for you without needing to run any manual bash script in your continuous integration process. **This means that you only have to run `composer test` instead of having to run a bash script to setup WordPress, rsync it to a temporary folder, and then run your tests.** Internally, Mantle will run a [shell script](https://github.com/alleyinteractive/mantle-ci/blob/HEAD/install-wp-tests.sh) that will install WordPress for you at a temporary directory. For plugins, this is more than enough to provide a WordPress installation to run tests against. Your tests and project would remain where it is currently and the rest of WordPress would be installed within a temporary directory. Themes or more integrated projects will need to [rsync your project](#rsyncing-your-project-to-a-wordpress-installation) to the temporary directory to run tests against. ##### Rsyncing Your Project to a WordPress Installation[​](#rsyncing-your-project-to-a-wordpress-installation "Direct link to Rsyncing Your Project to a WordPress Installation") Mantle can rsync your project to within a working WordPress installation without needing to run any rsync command yourself. This is useful for themes or more integrated projects that need to run tests against a fully integrated WordPress installation. Within your `tests/bootstrap.php` file, you can use the [Installation Manager](/docs/testing/installation-manager.md) to rsync your project to the WordPress installation: ```php // Rsync a plugin to live as a plugin within a WordPress installation. \Mantle\Testing\manager() ->maybe_rsync_plugin() ->install(); // Rsync a theme to live as a theme within a WordPress installation. \Mantle\Testing\manager() ->maybe_rsync_theme() ->install(); ``` For more information, read more about the [Installation Manager](/docs/testing/installation-manager.md). #### More Reading[​](#more-reading "Direct link to More Reading") Continue reading about the Mantle Testing Framework by checking out the following sections: #### [📄️ Testkit](/docs/testing/testkit.md) [Mantle Testkit is a standalone package for using the Mantle Testing Framework on non-Mantle based projects.](/docs/testing/testkit.md) #### [📄️ Installation Manager](/docs/testing/installation-manager.md) [The Installation Manager is a class used to install WordPress for testing.](/docs/testing/installation-manager.md) #### [📄️ HTTP Tests](/docs/testing/requests.md) [Mantle provides a fluent HTTP Request interface to make it easier to write feature/integration tests using PHPUnit and WordPress.](/docs/testing/requests.md) #### [📄️ Factory](/docs/testing/factory.md) [Mantle supports a WordPress-core backwards compatible factory that can be used in tests to quickly generate posts, terms, sites, and more.](/docs/testing/factory.md) #### [📄️ Assertions](/docs/testing/assertions.md) [Assertions available in Mantle's Test Case.](/docs/testing/assertions.md) #### [📄️ WordPress State](/docs/testing/state.md) [During unit tests, the testing framework exposes some helper methods to allow you to modify or inspect the state of WordPress.](/docs/testing/state.md) #### [📄️ Deprecation and Incorrect Usage](/docs/testing/deprecation-incorrect-usage.md) [Deprecation and incorrect usage notices can be captured and asserted against in Mantle's Test Case.](/docs/testing/deprecation-incorrect-usage.md) #### [📄️ Hooks](/docs/testing/hooks.md) [Mantle provides an interface for testing WordPress hooks in declarative and assertive formats.](/docs/testing/hooks.md) #### [📄️ Remote Requests](/docs/testing/remote-requests.md) [Mocking remote requests in unit tests.](/docs/testing/remote-requests.md) #### [📄️ Snapshot Testing](/docs/testing/snapshot-testing.md) [Snapshot testing in Mantle's testing framework.](/docs/testing/snapshot-testing.md) #### [📄️ Traits and Attributes](/docs/testing/traits-attributes.md) [Traits and attributes that can be used to add optional functionality to a test case.](/docs/testing/traits-attributes.md) #### [📄️ Users and Authentication](/docs/testing/users.md) [The Mantle Test Framework provides methods and assertions for testing users and authentication.](/docs/testing/users.md) #### [📄️ Cron and Queue](/docs/testing/cron.md) [Cron and queue jobs can be asserted in unit tests.](/docs/testing/cron.md) #### [📄️ Helpers](/docs/testing/helpers.md) [Helpers to make it easier to write tests for your project.](/docs/testing/helpers.md) #### [📄️ Continuous Integration](/docs/testing/continuous-integration.md) [Using Continuous Integration (CI) in your development can help ease your mind when adding new features to a site. This guide will help you setup your Mantle application or project that is using Mantle's testing framework for CI via GitHub Actions.](/docs/testing/continuous-integration.md) #### [📄️ Parallel Testing](/docs/testing/parallel.md) [Run your WordPress unit tests in parallel to speed up your test suite.](/docs/testing/parallel.md) #### [📄️ Pest](/docs/testing/pest.md) [Mantle's Testing Framework supports running unit tests via Pest.](/docs/testing/pest.md) #### [📄️ Environmental Variables](/docs/testing/environmental-variables.md) [Environmental variables used within the testing framework.](/docs/testing/environmental-variables.md) #### [📄️ Migration from PHPUnit 9](/docs/testing/migration-phpunit-9.md) [Mantle's Test Framework 1.0 upgrades the PHPUnit version to 10.x. This is a](/docs/testing/migration-phpunit-9.md) --- ### Testing: Assertions #### Introduction[​](#introduction "Direct link to Introduction") Mantle's Test Case provides some assertions above and beyond PHPUnit's, largely influenced by `WP_UnitTestCase`. Here's a run-through: #### `assertWPError` and `assertNotWPError`[​](#assertwperror-and-assertnotwperror "Direct link to assertwperror-and-assertnotwperror") Assert the given item is/is not a `WP_Error`: ```php $this->assertWPError( $actual, $message = '' ); $this->assertNotWPError( $actual, $message = '' ); ``` #### General Assertions[​](#general-assertions "Direct link to General Assertions") ##### assertEqualFields[​](#assertequalfields "Direct link to assertEqualFields") Asserts that the given fields are present in the given object: ```php $this->assertEqualFields( $object, $fields ); ``` ##### assertDiscardWhitespace[​](#assertdiscardwhitespace "Direct link to assertDiscardWhitespace") Asserts that two values are equal, with whitespace differences discarded: ```php $this->assertDiscardWhitespace( $expected, $actual ); ``` ##### assertEqualsIgnoreEOL[​](#assertequalsignoreeol "Direct link to assertEqualsIgnoreEOL") Asserts that two values are equal, with EOL differences discarded: ```php $this->assertEqualsIgnoreEOL( $expected, $actual ); ``` ##### assertEqualSets[​](#assertequalsets "Direct link to assertEqualSets") Asserts that the contents of two un-keyed, single arrays are equal, without accounting for the order of elements: ```php $this->assertEqualSets( $expected, $actual ); ``` ##### assertEqualSetsWithIndex[​](#assertequalsetswithindex "Direct link to assertEqualSetsWithIndex") Asserts that the contents of two keyed, single arrays are equal, without accounting for the order of elements: ```php $this->assertEqualSetsWithIndex( $expected, $actual ); ``` ##### assertNonEmptyMultidimensionalArray[​](#assertnonemptymultidimensionalarray "Direct link to assertNonEmptyMultidimensionalArray") Asserts that the given variable is a multidimensional array, and that all arrays are non-empty: ```php $this->assertNonEmptyMultidimensionalArray( $array ); ``` #### WordPress Query Assertions[​](#wordpress-query-assertions "Direct link to WordPress Query Assertions") ##### assertQueryTrue[​](#assertquerytrue "Direct link to assertQueryTrue") Assert that the global WordPress query is true for a given set of properties and false for the rest: ```php $this->assertQueryTrue( ...$prop ); ``` ##### assertQueriedObjectId[​](#assertqueriedobjectid "Direct link to assertQueriedObjectId") Assert that the global queried object ID is equal to the given ID: ```php $this->assertQueriedObjectId( int $id ); ``` ##### assertNotQueriedObjectId[​](#assertnotqueriedobjectid "Direct link to assertNotQueriedObjectId") Assert that the global queried object ID is not equal to the given ID: ```php $this->assertNotQueriedObjectId( int $id ); ``` ##### assertQueriedObject[​](#assertqueriedobject "Direct link to assertQueriedObject") Assert that the global queried object is equal to the given object: ```php $this->assertQueriedObject( $object ); ``` ##### assertNotQueriedObject[​](#assertnotqueriedobject "Direct link to assertNotQueriedObject") Assert that the global queried object is not equal to the given object: ```php $this->assertNotQueriedObject( $object ); ``` ##### assertQueriedObjectNull[​](#assertqueriedobjectnull "Direct link to assertQueriedObjectNull") Assert that the global queried object is null: ```php $this->assertQueriedObjectNull(); ``` #### WordPress Post/Term Existence[​](#wordpress-postterm-existence "Direct link to WordPress Post/Term Existence") ##### assertPostExists[​](#assertpostexists "Direct link to assertPostExists") Assert if a post exists given a set of arguments. ```php $this->assertPostExists( array $args ); ``` ##### assertPostDoesNotExists[​](#assertpostdoesnotexists "Direct link to assertPostDoesNotExists") Assert if a post doesn't exist given a set of arguments. ```php $this->assertPostDoesNotExists( array $args ); ``` ##### assertTermExists[​](#asserttermexists "Direct link to assertTermExists") Assert if a term exists given a set of arguments. ```php $this->assertTermExists( array $args ); ``` ##### assertTermDoesNotExists[​](#asserttermdoesnotexists "Direct link to assertTermDoesNotExists") Assert if a term doesn't exists given a set of arguments. ```php $this->assertTermDoesNotExists( array $args ); ``` ##### assertUserExists[​](#assertuserexists "Direct link to assertUserExists") Assert if a user exists given a set of arguments. ```php $this->assertUserExists( array $args ); ``` ##### assertUserDoesNotExists[​](#assertuserdoesnotexists "Direct link to assertUserDoesNotExists") Assert if a user doesn't exists given a set of arguments. ```php $this->assertUserDoesNotExists( array $args ); ``` ##### assertPostHasTerm[​](#assertposthasterm "Direct link to assertPostHasTerm") Assert if a post has a specific term. ```php $this->assertPostHasTerm( $post, $term ); ``` ##### assertPostNotHasTerm[​](#assertpostnothasterm "Direct link to assertPostNotHasTerm") Assert if a post does not have a specific term (aliased to `assertPostsDoesNotHaveTerm()`) ```php $this->assertPostNotHasTerm( $post, $term ); ``` #### Asset Assertions[​](#asset-assertions "Direct link to Asset Assertions") Assets that are registered and/or enqueued with WordPress can be asserted against using the following methods: ##### assertScriptStatus[​](#assertscriptstatus "Direct link to assertScriptStatus") Assert against the status of a script. `$status` accepts 'enqueued', 'registered', 'queue', 'to\_do', and 'done'. ```php $this->assertScriptStatus( string $handle, string $status ); ``` ##### assertStyleStatus[​](#assertstylestatus "Direct link to assertStyleStatus") Assert against the status of a style. `$status` accepts 'enqueued', 'registered', 'queue', 'to\_do', and 'done'. ```php $this->assertStyleStatus( string $handle, string $status ); ``` ##### assertScriptEnqueued[​](#assertscriptenqueued "Direct link to assertScriptEnqueued") Assert that a script is enqueued by handle. ```php $this->assertScriptEnqueued( string $handle ); ``` ##### assertScriptNotEnqueued[​](#assertscriptnotenqueued "Direct link to assertScriptNotEnqueued") Assert that a script is not enqueued by handle. ```php $this->assertScriptNotEnqueued( string $handle ); ``` ##### assertStyleEnqueued[​](#assertstyleenqueued "Direct link to assertStyleEnqueued") Assert that a style is enqueued by handle. ```php $this->assertStyleEnqueued( string $handle ); ``` ##### assertStyleNotEnqueued[​](#assertstylenotenqueued "Direct link to assertStyleNotEnqueued") Assert that a style is not enqueued by handle. ```php $this->assertStyleNotEnqueued( string $handle ); ``` ##### assertScriptRegistered[​](#assertscriptregistered "Direct link to assertScriptRegistered") Assert that a script is registered. ```php $this->assertScriptRegistered( string $handle ); ``` ##### assertStyleRegistered[​](#assertstyleregistered "Direct link to assertStyleRegistered") Assert that a style is registered. ```php $this->assertStyleRegistered( string $handle ); ``` #### Block Assertions[​](#block-assertions "Direct link to Block Assertions") Powered by [Match Blocks](https://github.com/alleyinteractive/wp-match-blocks). ##### assertStringMatchesBlock[​](#assertstringmatchesblock "Direct link to assertStringMatchesBlock") Assert that a string matches a block with the given optional attributes. ```php $this->assertStringMatchesBlock( $string, $args ); ``` ##### assertStringNotMatchesBlock[​](#assertstringnotmatchesblock "Direct link to assertStringNotMatchesBlock") Assert that a string does not match a block with the given optional attributes. ```php $this->assertStringNotMatchesBlock( $string, $args ); ``` ##### assertStringHasBlock[​](#assertstringhasblock "Direct link to assertStringHasBlock") Assert that a string has a block with the given optional attributes. ```php $this->assertStringHasBlock( $string, $block_name, $attrs ); ``` ##### assertStringNotHasBlock[​](#assertstringnothasblock "Direct link to assertStringNotHasBlock") Assert that a string does not have a block with the given optional attributes. ```php $this->assertStringNotHasBlock( string $string, array|string $block_name, array $attrs = [] ); ``` ##### assertPostHasBlock[​](#assertposthasblock "Direct link to assertPostHasBlock") Assert that a post has a block in its content with the given optional attributes. ```php use Mantle\Database\Model\Post; $this->assertPostHasBlock( Post|\WP_Post $post, string|array $block_name, array $attrs = [] ); // Example: $this->assertPostHasBlock( $post, 'core/paragraph' ); $this->assertPostHasBlock( $post, 'core/paragraph', [ 'placeholder' => 'Hello, world!' ] ); // Example with multiple blocks: $this->assertPostHasBlock( $post, [ 'core/paragraph', 'core/image' ] ); $this->assertPostHasBlock( $post, [ 'core/paragraph', 'core/image' ], [ 'placeholder' => 'Hello, world!' ] ); ``` ##### assertPostNotHasBlock[​](#assertpostnothasblock "Direct link to assertPostNotHasBlock") Assert that a post does not have a block in its content with the given optional attributes. ```php use Mantle\Database\Model\Post; $this->assertPostNotHasBlock( Post|\WP_Post $post, string|array $block_name, array $attrs ); // Example: $this->assertPostNotHasBlock( $post, 'core/paragraph' ); $this->assertPostNotHasBlock( $post, 'core/paragraph', [ 'placeholder' => 'Hello, world!' ] ); // Example with multiple blocks (matching any of the blocks will fail): $this->assertPostNotHasBlock( $post, [ 'core/paragraph', 'core/image' ] ); $this->assertPostNotHasBlock( $post, [ 'core/paragraph', 'core/image' ], [ 'placeholder' => 'Hello, world!' ] ); ``` #### Mail Assertions[​](#mail-assertions "Direct link to Mail Assertions") ##### assertMailSent[​](#assertmailsent "Direct link to assertMailSent") Assert that an email was sent to the given address or callback. ```php $this->assertMailSent( 'example@example.com' ); ``` You can also pass a callback to assert that an email was sent to any address matching the callback. The callback will be passed an instance of `\Mantle\Testing\Mail\Mail_Message`. ```php use Mantle\Testing\Mail\Mail_Message; $this->assertMailSent( fn ( Mail_Message $message ) => $message->to === 'example@example.com' && $message->subject === 'Hello, world!', ); ``` ##### assertMailNotSent[​](#assertmailnotsent "Direct link to assertMailNotSent") Assert that an email was not sent to the given address or callback. ```php $this->assertMailNotSent( 'example@example.com' ); ``` You can also pass a callback to assert that an email was not sent to any address matching the callback. The callback will be passed an instance of `\Mantle\Testing\Mail\Mail_Message`. ```php use Mantle\Testing\Mail\Mail_Message; $this->assertMailNotSent( fn ( Mail_Message $message ) => $message->to === 'example@example.com, ); ``` #### assertMailSentCount[​](#assertmailsentcount "Direct link to assertMailSentCount") Assert that the given number of emails were sent to the given address or any address matching the callback. ```php $this->assertMailSentCount( 3 ); $this->assertMailSentCount( 3, 'example@example.com' ); $this->assertMailSentCount( 3, fn ( Mail_Message $message ) => $message->to === 'example@example.com' ); ``` #### HTTP Test Assertions[​](#http-test-assertions "Direct link to HTTP Test Assertions") See [HTTP Testing](/docs/testing/requests.md#available-assertions) for a full list of HTTP test assertions. --- ### Testing: Continuous Integration #### Introduction[​](#introduction "Direct link to Introduction") Using Continuous Integration (CI) in your development can help ease your mind when adding new features to a site. This guide will help you setup your Mantle application or project that is using Mantle's testing framework for CI via GitHub Actions. Are you transitioning an existing site to Mantle's Test Framework? Be sure to checkout [Transitioning to Test Framework using Mantle Testkit](/docs/testing/testkit.md) for more information. #### Differences from Core Tests[​](#differences-from-core-tests "Direct link to Differences from Core Tests") One difference to call out from core is that Mantle does not require the use of `bin/install-wp-tests.sh` to run tests in a CI environment. (If you don't know what that is, skip ahead!) Mantle will automatically attempt to download and install WordPress behind the scenes so that your plugin/package can focus on other things. For majority of the use cases plugins/packages will be able to run `./vendor/bin/phpunit` and have their tests run automatically. For more information, read more about the [Installation Manager](/docs/testing/installation-manager.md). #### Environment Variables[​](#environment-variables "Direct link to Environment Variables") By default, no variables need to be set to run your tests. It is recommended to set the following variables before running: * `CACHEDIR`: `/tmp/test-cache` * `WP_CORE_DIR`: `/tmp/wordpress` The `CACHEDIR` variable defines the location of the cache folder used for tests. If possible, cache that folder and use it across tests to improve performance. For tests using the Mantle installation script, `WP_DEVELOP_DIR` and `WP_TESTS_DIR` are unused and do not need to be defined. For more environmental variables, see the [Installation Manager](/docs/testing/installation-manager.md#overriding-the-default-installation). Where can I find the Mantle version of the installation script? The canonical version of the Mantle `install-wp-tests.sh` script is located [here](https://github.com/alleyinteractive/mantle-ci/blob/HEAD/install-wp-tests.sh) and can be referenced in tests via `https://raw.githubusercontent.com/alleyinteractive/mantle-ci/HEAD/install-wp-tests.sh`. To manually install WordPress, run the following commands inside your integration test (specific examples for [GitHub Actions](#github-actions) can be found below): ```bash # Testing: Install Composer composer install # Testing: Run PHPUnit/phpcs composer test ``` An example test bootstrap file for a plugin [can be found here](https://github.com/alleyinteractive/mantle/blob/HEAD/tests/bootstrap.php). #### Caching[​](#caching "Direct link to Caching") Caching can improve the performance of your tests by a great deal. Your tests should cache the dependencies installed using Composer and the remote files downloaded during testing (the `CACHEDIR` variable). The configurations included in this guide will use the recommended caching for testing. The Mantle version of the `install-wp-tests.sh` script will also cache the WordPress installation and latest WordPress version for 24 hours. #### Setting Up Continuous Integration[​](#setting-up-continuous-integration "Direct link to Setting Up Continuous Integration") ##### GitHub Actions[​](#github-actions "Direct link to GitHub Actions") The Mantle repository includes GitHub Actions for testing your Mantle application against PHPUnit and phpcs: * [GitHub Action Workflow](https://github.com/alleyinteractive/mantle/blob/HEAD/.github/workflows/all-pr-tests.yml) --- ### Testing: Cron / Queue #### Introduction[​](#introduction "Direct link to Introduction") Cron and queue jobs can be asserted in unit tests. #### Dispatching Cron[​](#dispatching-cron "Direct link to Dispatching Cron") The WordPress cron can be dispatched by calling `dispatch_cron()` and optionally passing the action name to run. ```php namespace App\Tests; class Dispatch_Cron_Test extends Test_Case { public function test_cron() { $this->dispatch_cron( 'example' ); // ... } } ``` #### Asserting Cron Actions[​](#asserting-cron-actions "Direct link to Asserting Cron Actions") The `assertInCronQueue()` and `assertNotInCronQueue()` methods can be used to assert if a cron action is in the queue. ```php namespace App\Tests; class Cron_Test extends Test_Case { public function test_cron() { $this->assertNotInCronQueue( 'example' ); wp_schedule_single_event( time(), 'example' ); $this->assertInCronQueue( 'example' ); $this->dispatch_cron( 'example' ); $this->assertNotInCronQueue( 'example' ); } } ``` #### Queue[​](#queue "Direct link to Queue") The [Mantle Queue](/docs/features/queue.md) can be run and asserted against in unit tests. ```php namespace App\Tests; use App\Jobs\Example_Job; class Example_Queue_Test extends Test_Case { public function test_queue() { $job = new Example_Job( 1, 2, 3 ); // Assert if a job class with a set of arguments is not in the queue. $this->assertJobNotQueued( Example_Job::class, [ 1, 2, 3 ] ); // Assert if a specific job is not in the queue. $this->assertJobNotQueued( $job ); Example_Job::dispatch( 1, 2, 3 ); $this->assertJobQueued( Example_Job::class, [ 1, 2, 3 ] ); $this->assertJobQueued( $job ); // Fire the queue. $this->dispatch_queue(); $this->assertJobNotQueued( Example_Job::class, [ 1, 2, 3 ] ); $this->assertJobNotQueued( $job ); } } ``` --- ### Testing: Deprecation and Incorrect Usage #### Introduction[​](#introduction "Direct link to Introduction") Deprecation notices from `_deprecated_function()` and `_deprecated_argument()` and incorrect usage notices from `_doing_it_wrong()` are captured and can be asserted against. By default, deprecation and incorrect usage notices will throw an error and fail the test. tip When you ignore a deprecation or incorrect usage notice, you can use a `*` as a wildcard to ignore all or part of the notice. This applies to all methods of declaring expectations below. #### Declaring Expectations with Attributes[​](#declaring-expectations-with-attributes "Direct link to Declaring Expectations with Attributes") A test can declare a expected deprecation or incorrect usage notice or ignore a deprecation or incorrect usage notice by using the following attributes: * `Mantle\Testing\Attributes\Expected_Deprecation`: Declare that a test case is expected to trigger a deprecation notice for a function. * `Mantle\Testing\Attributes\Expected_Incorrect_Usage`: Declare that a test case is expected to trigger a doing it wrong notice for a function. * `Mantle\Testing\Attributes\Ignore_Deprecation`: Ignore a deprecation notice for a specific function or all functions. * `Mantle\Testing\Attributes\Ignore_Incorrect_Usage`: Ignore a doing it wrong notice for a specific function or all functions. ##### Expected Deprecation[​](#expected-deprecation "Direct link to Expected Deprecation") Using the `Expected_Deprecation` attribute, you can declare that a test case or a test method is expected to trigger a deprecation notice for a function. note If a deprecation notice is not triggered, the test will fail. ```php use Mantle\Testing\Attributes\Expected_Deprecation; use Tests\Test_Case; #[Expected_Deprecation( 'example_function' )] class ExampleAttributeTest extends Test_Case { public function test_deprecated_function() { $this->assertTrue( true ); } } ``` You can also expect a deprecation notice for a single test method: ```php use Mantle\Testing\Attributes\Expected_Deprecation; use Tests\Test_Case; class ExampleAttributeTest extends Test_Case { #[Expected_Deprecation( 'example_function' )] public function test_deprecated_function() { $this->assertTrue( true ); } } ``` ##### Expected Incorrect Usage[​](#expected-incorrect-usage "Direct link to Expected Incorrect Usage") Using the `Expected_Incorrect_Usage` attribute, you can declare that a test case or a test method is expected to trigger a doing it wrong notice for a function. note If a doing it wrong notice is not triggered, the test will fail. ```php use Mantle\Testing\Attributes\Expected_Incorrect_Usage; use Tests\Test_Case; #[Expected_Incorrect_Usage( 'example_function' )] class ExampleAttributeTest extends Test_Case { public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` You can also expect a doing it wrong notice for a single test method: ```php use Mantle\Testing\Attributes\Expected_Incorrect_Usage; use Tests\Test_Case; class ExampleAttributeTest extends Test_Case { #[Expected_Incorrect_Usage( 'example_function' )] public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` ##### Ignore Deprecation[​](#ignore-deprecation "Direct link to Ignore Deprecation") Using the `Ignore_Deprecation` attribute, you can ignore deprecation notices for a specific function or all functions. By default, it will ignore deprecation notices unless you pass a `$deprecated` parameter. ```php use Mantle\Testing\Attributes\Ignore_Deprecation; use Tests\Test_Case; #[Ignore_Deprecation( 'example_function' )] class ExampleAttributeTest extends Test_Case { public function test_deprecated_function() { $this->assertTrue( true ); } } ``` You can also ignore deprecation notices for a single test method: ```php use Mantle\Testing\Attributes\Ignore_Deprecation; use Tests\Test_Case; class ExampleAttributeTest extends Test_Case { #[Ignore_Deprecation( 'example_function' )] public function test_deprecated_function() { $this->assertTrue( true ); } } ``` You can also ignore all deprecation notices: ```php use Mantle\Testing\Attributes\Ignore_Deprecation; use Tests\Test_Case; #[Ignore_Deprecation] class ExampleAttributeTest extends Test_Case { public function test_deprecated_function() { $this->assertTrue( true ); } } ``` ##### Ignore Incorrect Usage[​](#ignore-incorrect-usage "Direct link to Ignore Incorrect Usage") Using the `Ignore_Incorrect_Usage` attribute, you can ignore doing it wrong notices for a specific function or all functions. By default, it will ignore doing it wrong notices unless you pass a `$name` parameter. ```php use Mantle\Testing\Attributes\Ignore_Incorrect_Usage; use Tests\Test_Case; #[Ignore_Incorrect_Usage( 'example_function' )] class ExampleAttributeTest extends Test_Case { public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` You can also ignore doing it wrong notices for a single test method: ```php use Mantle\Testing\Attributes\Ignore_Incorrect_Usage; use Tests\Test_Case; class ExampleAttributeTest extends Test_Case { #[Ignore_Incorrect_Usage( 'example_function' )] public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` You can also ignore all doing it wrong notices: ```php use Mantle\Testing\Attributes\Ignore_Incorrect_Usage; use Tests\Test_Case; #[Ignore_Incorrect_Usage] class ExampleAttributeTest extends Test_Case { public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` #### Declaring Expectations with Methods[​](#declaring-expectations-with-methods "Direct link to Declaring Expectations with Methods") You can also declare a expected deprecation/incorrect usage notice or ignore a deprecation/incorrect usage notice by calling the following methods: * `setExpectedDeprecated( string $deprecated )`: Declare that a test case is expected to trigger a deprecation notice for a function. * `setExpectedIncorrectUsage( string $doing_it_wrong )`: Declare that a test case is expected to trigger a doing it wrong notice for a function. * `ignoreDeprecated( string $deprecated = '*' )`: Ignore a deprecation notice for a specific function or all functions. * `ignoreDeprecated( string $deprecated = '*' )`: Ignore a doing it wrong notice for a specific function or all functions. ##### Expected Deprecation[​](#expected-deprecation-1 "Direct link to Expected Deprecation") Using the `setExpectedDeprecated()` method, you can declare that a test is expected to trigger a deprecation notice for a function. note If a deprecation notice is not triggered, the test will fail. ```php use Tests\Test_Case; class ExampleMethodTest extends Test_Case { public function test_deprecated_function() { $this->setExpectedDeprecated( 'example_function' ); $this->assertTrue( true ); } } ``` ##### Expected Incorrect Usage[​](#expected-incorrect-usage-1 "Direct link to Expected Incorrect Usage") Using the `setExpectedIncorrectUsage()` method, you can declare that a test is expected to trigger a doing it wrong notice for a function. note If a doing it wrong notice is not triggered, the test will fail. ```php use Tests\Test_Case; class ExampleMethodTest extends Test_Case { public function test_incorrect_usage_function() { $this->setExpectedIncorrectUsage( 'example_function' ); $this->assertTrue( true ); } } ``` ##### Ignore Deprecation[​](#ignore-deprecation-1 "Direct link to Ignore Deprecation") Using the `ignoreDeprecated()` method, you can ignore deprecation notices for a test. By default, it will ignore deprecation notices unless you pass a `$deprecated` parameter. ```php use Tests\Test_Case; class ExampleMethodTest extends Test_Case { public function test_deprecated_function() { // Ignore a specific function. $this->ignoreDeprecated( 'example_function' ); // Ignore all functions that start with `wp_`. $this->ignoreDeprecated( 'wp_*' ); // Ignore all deprecation notices. $this->ignoreDeprecated(); } } ``` ##### Ignore Incorrect Usage[​](#ignore-incorrect-usage-1 "Direct link to Ignore Incorrect Usage") Using the `ignoreDeprecated()` method, you can ignore doing it wrong notices for a test.. By default, it will ignore doing it wrong notices unless you pass a `$name` parameter. ```php use Tests\Test_Case; class ExampleMethodTest extends Test_Case { public function test_incorrect_usage_function() { // Ignore a specific function. $this->ignoreIncorrectUsage( 'example_function' ); // Ignore all functions that start with `wp_`. $this->ignoreIncorrectUsage( 'wp_*' ); // Ignore all doing it wrong notices. $this->ignoreIncorrectUsage(); $this->assertTrue( true ); } } ``` #### Expecting Expectations with PHPDoc Annotations[​](#expecting-expectations-with-phpdoc-annotations "Direct link to Expecting Expectations with PHPDoc Annotations") As with in `WP_UnitTestCase`, you can add PHPDoc annotations to a test method that will flag a test as expecting a deprecation or incorrect usage notice. Using Annotations is Deprecated This format of declaring expected deprecations or incorrect usage was deprecated in PHPUnit 11 and removed with PHPUnit 12. While framework does support PHPUnit 10 still, this format of declaring expected deprecations or incorrect usage will not work if you are using a newer version of PHPUnit. Mantle 2.0 will be dropping support for PHPUnit 10 and this format will no longer work. You should use [attributes](#declaring-expectations-with-attributes) or [methods](#declaring-expectations-with-methods) to declare expected deprecations or incorrect usage. **Do not write new code using this format.** note If a test has an `@expectedDeprecated` or `@expectedIncorrectUsage` annotation, it will not fail if a deprecation or incorrect usage notice is not triggered. If the test does not cause a deprecation or incorrect usage notice, it will fail. ```php use Tests\Test_Case; class Example_Annotation_Test extends Test_Case { /** * @expectedDeprecated example_function */ public function test_deprecated_function() { $this->assertTrue( true ); } /** * @expectedIncorrectUsage example_function */ public function test_incorrect_usage_function() { $this->assertTrue( true ); } } ``` --- ### Testing: Environmental Variables This document describes the environmental variables that are used within the testing framework and their purpose. Most variables are used during installation and can be configured using methods on the [Installation Manager](/docs/testing/installation-manager.md). | Variable | Description | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CACHEDIR` | The directory used when downloading WordPress, WordPress plugins, and checking WordPress versions during testing. [Defaults to `/tmp`.](https://github.com/alleyinteractive/mantle-ci/blob/de3063db39a21cf019490c2018a3d7ccd35ac557/install-plugin.sh#L72-L73) | | `DISABLE_WP_UNIT_TEST_CASE_SHIM` | Disable the loading of a shim-version of `WP_UnitTestCase` that is used to support transitioning to Mantle Testkit. | | `MANTLE_CI_BRANCH` | The branch of the Mantle CI repository to use when installing WordPress. | | `MANTLE_EXPERIMENTAL_TESTING_USE_HOME_URL_HOST` | [See documentation](/docs/testing/installation-manager.md#using-the-experimental-feature-for-home-url-in-testing). | | `MANTLE_INSTALL_OBJECT_CACHE` | Object cache to install during testing. [See documentation](/docs/testing/installation-manager.md#including-memcache-object-cache-drop-in). | | `MANTLE_INSTALL_VIP_MU_PLUGINS` | Install VIP MU plugins during testing. [See documentation](/docs/testing/installation-manager.md#including-wordpress-vip-mu-plugins). | | `MANTLE_LOAD_VIP_CONFIG` | Override to disable the loading of a `vip-config/vip-config.php` file in the `wp-config.php`. Defaults to `true`. | | `MANTLE_REQUIRE_OBJECT_CACHE` | Force Mantle to install an object cache even if the environment does not support it. | | `MANTLE_SKIP_LOCAL_OBJECT_CACHE` | Skip the loading of a local object cache drop-in during local testing. [See documentation](/docs/testing/installation-manager.md#disabling-object-cache-for-local-development). | | `MANTLE_USE_SQLITE` | Use SQLite for the database connection. [See documentation](/docs/testing/installation-manager.md#using-sqlite-for-the-database). | | `WP_DB_CHARSET` | The database character set. Defaults to `utf8`. | | `WP_DB_COLLATE` | The database collation. Defaults to `(empty)`. | | `WP_DB_HOST` | The database host. Defaults to `localhost`. | | `WP_DB_NAME` | The database name. Defaults to `wordpress_unit_tests`. | | `WP_DB_USER` | The database user. Defaults to `root`. | | `WP_DB_PASSWORD` | The database password. Defaults to `root`. | | `WP_DEFAULT_THEME` | The default theme to use during testing. Defaults to `default`. | | `WP_MULTISITE` | Flag to install as multisite when testing. Defaults to `0`. | | `WP_SKIP_DB_CREATE` | Skip creating the database during testing. Defaults to `0`. | | `WP_TESTS_DOMAIN` | The domain to use during testing. Defaults to `example.org`. [See documentation](/docs/testing/installation-manager.md#using-the-experimental-feature-for-home-url-in-testing). | | `WP_TESTS_USE_HTTPS` | Flag to use HTTPS during testing. Defaults to `0`. [See documentation](/docs/testing/installation-manager.md#using-the-experimental-feature-for-home-url-in-testing). | | `WP_VERSION` | The version of WordPress to install during testing. Defaults to `latest`. | --- ### Testing: Factory Mantle supports a WordPress-core backwards compatible factory that can be used in tests to quickly generate posts, terms, sites, and more. The factory supports the creation of the following types of content: * attachment * category * comment * page * post * tag * term * user * blog (if mulitisite) * network (if multisite) * Co-Authors Plus guest authors (if available) * Byline Manager profiles (if available) note The testing factory used by the testing framework is the same factory used by Mantle's database factory. For more information on the database factory, see the [Database Factory](/docs/models/database-factory.md) documentation. #### Generating Content[​](#generating-content "Direct link to Generating Content") All factories function in the same manner and be used to generate one or many pieces of content: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_factory() { static::factory()->post->create_and_get(); // WP_Post static::factory()->post->create(); // int static::factory()->post->create_many( 10 ); // int[] // Supports overriding attributes for a factory. static::factory()->post->create( [ 'title' => 'Example Title' ] ); static::factory()->tag->create_and_get(); // WP_Term static::factory()->tag->create(); // int } } ``` For more information on how to generate content with factories, see the [Model Factory: Generated Content](/docs/models/database-factory.md#generating-content) documentation. Factories for custom post types/taxonomies can be instantiated by calling the relevant post type/taxonomy name when retrieving the factory: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_factory() { static::factory()->custom_post_type->create_and_get(); // WP_Post static::factory()->custom_post_type->create(); // int static::factory()->custom_post_type->create_many( 10 ); // int[] static::factory()->custom_taxonomy->create_and_get(); // WP_Term static::factory()->custom_taxonomy->create(); // int } } ``` #### Generating Blocks[​](#generating-blocks "Direct link to Generating Blocks") Generating blocks is a common use case when testing WordPress sites that depend on blocks in different contexts for testing. The `block_factory()` method can be used to generate blocks for testing: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; use function Mantle\Testing\block_factory; class ExampleTests extends Testkit_Test_Case { public function test_post_with_block() { // Create a post with a heading and paragraph block. $post = static::factory()->post->create_and_get( [ 'post_content' => block_factory()->blocks( [ block_factory()->heading( 'Example Heading' ), block_factory()->paragraph( 'Example Paragraph' ), ] ), ] ); // ... } } ``` The Block Factory works with the [Block Faker](/docs/models/database-factory.md#generating-blocks-in-factories) to make it possible to generate blocks in tests with ease. ##### Generating Paragraph Blocks[​](#generating-paragraph-blocks "Direct link to Generating Paragraph Blocks") The Block Factory's `block_factory()->paragraph()` method can be used to generate a paragraph block with the specified content or default lorem ipsum content: ```php use function Mantle\Testing\block_factory; $block = block_factory()->paragraph(); ``` Which would produce the following block: ```html

Dolores esse et quam dolores perspiciatis. Ut et et dolor voluptate quia ipsam distinctio. Saepe eum placeat dolor saepe ut cum officia.

``` The text can be overridden by passing a string to the `paragraph` method: ```php use function Mantle\Testing\block_factory; $block = block_factory()->paragraph( 'Example Paragraph' ); ``` Which would produce the following block: ```html

Example Paragraph

``` You can also specify the number of sentences to generate for the paragraph: ```php use function Mantle\Testing\block_factory; $block = block_factory()->paragraph( sentences: 5 ); ``` ##### Generating Multiple Paragraph Blocks[​](#generating-multiple-paragraph-blocks "Direct link to Generating Multiple Paragraph Blocks") The Block Factory's `block_factory()->paragraphs()` method can be used to generate multiple paragraph blocks with the specified content or default lorem ipsum content: ```php use function Mantle\Testing\block_factory; $blocks = block_factory()->paragraphs( 3 ); ``` ##### Generating Heading Blocks[​](#generating-heading-blocks "Direct link to Generating Heading Blocks") The Block Factory's `block_factory()->heading()` method can be used to generate a heading block with the specified content or default lorem ipsum content: ```php use function Mantle\Testing\block_factory; $block = block_factory()->heading(); ``` Which would produce the following block: ```html

Dolores esse et quam dolores perspiciatis.

``` The text can be overridden by passing a string to the `heading` method: ```php use function Mantle\Testing\block_factory; $block = block_factory()->heading( 'Example Heading' ); ``` Which would produce the following block: ```html

Example Heading

``` You can also specify the heading level to generate: ```php use function Mantle\Testing\block_factory; $block = block_factory()->heading( level: 3 ); ``` ##### Generating Image Blocks[​](#generating-image-blocks "Direct link to Generating Image Blocks") The Block Factory's `block_factory()->image()` method can be used to generate an image block with a default image from picsum.photos: ```php use function Mantle\Testing\block_factory; $block = block_factory()->image(); ``` Which would produce the following block: ```html
``` ##### Generating a Custom Block[​](#generating-a-custom-block "Direct link to Generating a Custom Block") The Block Factory's `block` method can be used to generate a custom block with the specified block name and attributes: ```php use function Mantle\Testing\block_factory; $block = block_factory()->block( 'vendor/block-name', 'Inner content (if any).', [ 'attribute' => 'value', ], ); ``` ##### Generating Sets of Blocks[​](#generating-sets-of-blocks "Direct link to Generating Sets of Blocks") The Block Factory's `blocks` method can be used to generate a set of blocks: ```php use function Mantle\Testing\block_factory; $blocks = block_factory()->blocks( [ block_factory()->heading( 'Example Heading' ), block_factory()->paragraph( 'Example Paragraph' ), ] ); ``` Which would produce the following blocks: ```html

Example Heading

Example Paragraph

``` ##### Using Block Presets[​](#using-block-presets "Direct link to Using Block Presets") Block presets can be used to generate blocks with a specific set of attributes. This can be useful when a project has a common set of blocks that are used across multiple tests. For example, a media website may have a post with a common template of a custom block, a heading, and a paragraph. A block preset can be created to generate this set of blocks: ```php use Mantle\Testing\Block_Factory; use function Mantle\Testing\block_factory; Block_Factory::register_preset( name: 'article_3_up', preset: block_factory()->blocks( [ block_factory()->block( 'vendor/block-name', '', [ 'attribute' => 'value', ] ), block_factory()->heading(), block_factory()->paragraph(), ] ), ); ``` The block preset can then be used in tests: ```php use function Mantle\Testing\block_factory; $blocks = block_factory()->preset( 'article_3_up' ); // You can also use a magic method to call it directly. $blocks = block_factory()->article_3_up(); ``` Presets can also be a callable function that returns a set of blocks. The callable will be passed an instance of the Block Factory: ```php use Mantle\Testing\Block_Factory; Block_Factory::register_preset( name: 'content_with_video', preset: fn ( Block_Factory $factory ) => $factory->blocks( [ $factory->block( 'vendor/block-name', '', [ 'attribute' => 'value', ] ), $factory->heading(), $factory->paragraph(), ] ), ); ``` The preset callback will forward any arguments passed to the preset method: ```php use function Mantle\Testing\block_factory; Block_Factory::register_preset( name: 'content_with_video', preset: fn ( Block_Factory $factory, string $url ) => $factory->blocks( [ $factory->block( 'vendor/block-name', '', [ 'attribute' => 'value', 'url' => $url, // The passed argument. ] ), $factory->heading(), $factory->paragraph(), ] ), ); ``` #### Generating Co-Authors Plus Guest Authors[​](#generating-co-authors-plus-guest-authors "Direct link to Generating Co-Authors Plus Guest Authors") The factory supports the generation of [Co-Authors Plus](https://wordpress.org/plugins/co-authors-plus/) Guest Authors if the plugin is available: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_guest_author(): void { $author = static::factory()->cap_guest_author->create_and_get(); $this->assertInstanceOf( stdClass::class, $author ); $this->assertNotEmpty( $author->display_name ); $this->assertNotEmpty( get_post_meta( $author->ID, 'cap-first_name', true ) ); $this->assertNotEmpty( get_post_meta( $author->ID, 'cap-last_name', true ) ); } } ``` You can also pass along data to the factory to override the default values: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_guest_author(): void { $author = static::factory()->cap_guest_author->create_and_get( [ 'display_name' => 'John Doe', 'first_name' => 'John', 'last_name' => 'Doe', ] ); $this->assertInstanceOf( stdClass::class, $author ); $this->assertSame( 'John Doe', $author->display_name ); $this->assertSame( 'John', get_post_meta( $author->ID, 'cap-first_name', true ) ); $this->assertSame( 'Doe', get_post_meta( $author->ID, 'cap-last_name', true ) ); } } ``` A guest author can be linked to a WordPress user and inherit data from that user: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_guest_author_linked(): void { $user = static::factory()->user->create_and_get(); $author = static::factory()->cap_guest_author->with_linked_user( $user->ID )->create_and_get(); // ... } } ``` You can create content with a guest author as the author of a post (supports passing a guest author or a WordPress user): ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_post_with_guest_author(): void { $author = static::factory()->cap_guest_author->create_and_get(); $post = static::factory()->post->with_cap_authors( $author )->create_and_get(); // ... } } ``` #### Generating Byline Manager Profiles[​](#generating-byline-manager-profiles "Direct link to Generating Byline Manager Profiles") The factory supports the generation of [Byline Manager](https://github.com/alleyinteractive/byline-manager) profiles posts with bylines: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_byline_manager_profile(): void { $profile = static::factory()->byline_manager_profile->create_and_get(); $this->assertInstanceOf( WP_Post::class, $profile ); // ... } } ``` You can also pass along data to the factory to override the default values: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_byline_manager_profile(): void { $profile = static::factory()->byline_manager_profile->create_and_get( [ 'display_name' => 'John Doe', ] ); // ... } } ``` A profile can be linked to a WordPress user and inherit data from that user: ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_byline_manager_profile_linked(): void { $user = static::factory()->user->create_and_get(); $profile = static::factory()->byline_manager_profile->with_linked_user( $user->ID )->create_and_get(); // ... } } ``` You can create content with a byline manager profile as the author of a post. This method supports a Byline Manager profile, WordPress user, or a string byline (with no linked user or profile): ```php use Mantle\Testkit\TestCase as Testkit_Test_Case; class ExampleTests extends Testkit_Test_Case { public function test_create_post_with_byline_manager_profile(): void { $profile = static::factory()->byline_manager_profile->create_and_get(); $post = static::factory()->post->with_byline( $profile )->create_and_get(); // ... } public function test_create_post_with_byline_manager_profile_and_text_byline(): void { $profile = static::factory()->byline_manager_profile->with_linked_user( $user->ID )->create_and_get(); $post = static::factory()->post->with_byline( $profile, $user, 'Another Person' )->create_and_get(); // ... } } ``` --- ### Testing: Helpers Mantle provides some testing helpers to make it easier to write tests for your project. #### HTML String[​](#html-string "Direct link to HTML String") The `Mantle\Testing\html_string` helper is used to create a DOMDocument object from an HTML string. This can be useful when you need to test a function that returns HTML output and want to make assertions about the generated HTML. You can use the same methods as the [Element Assertions](/docs/testing/requests.md#element-assertions) feature to make assertions about the HTML string. ```php use Mantle\Testing\html_string; html_string( '
Example Section
Example Div By Class
Example Div By ID
  • Item 1
  • Item 2
  • Item 3
' ) ->assertContains( 'Example Section' ) ->assertElementExists( '//section' ) ->assertQuerySelectorExists( 'section' ) ->assertElementExistsById( 'test-id' ) ->assertElementExistsByTestId( 'test-item' ); ``` The HTML string helper returns a [HTML](/docs/features/support/html.md) object. #### Getting Output[​](#getting-output "Direct link to Getting Output") The [`capture()`](/docs/features/support/helpers.md#capture) helper can be used to capture output from a callable. It is a replacement for the core's [`get_echo()` function](https://github.com/WordPress/wordpress-develop/blob/cf5898957e68d4d9fa63b5e89e2bee272391aa92/tests/phpunit/includes/utils.php#L432-L436). ```php use function Mantle\Support\Helpers\capture; $output = capture( fn () => the_title() ); ``` #### Time Mocking[​](#time-mocking "Direct link to Time Mocking") Mantle provides powerful time mocking capabilities to help you test time-dependent functionality in your application. Whether you need to test scheduled events, time-based calculations, or date-specific features, Mantle's time travel helpers make it simple to control the flow of time in your tests. tip All time modifications are automatically cleaned up after each test, so you don't have to worry about time changes affecting other tests. ##### Traveling Through Time[​](#traveling-through-time "Direct link to Traveling Through Time") The `travel()` method allows you to set the current time to a specific date and time. All calls to `now()` and `Carbon::now()` will return the time you've set until you travel back or your test ends. ```php use Mantle\Support\Carbon; use function Mantle\Support\Helpers\now; class ScheduledEventTest extends TestCase { public function test_event_fires_at_correct_time(): void { // Travel to a specific date and time $this->travel( Carbon::parse( '2024-01-15 10:00:00' ) ); // Now all time-based functions will return January 15, 2024 at 10:00 AM $this->assertEquals( '2024-01-15 10:00:00', now()->format( 'Y-m-d H:i:s' ) ); // Test your scheduled event logic here $event = ScheduledPost::create([ 'title' => 'Future Post', 'publish_at' => now()->addHours( 2 ), ]); $this->assertFalse( $event->isPublished() ); // Travel forward 2 hours $this->travel( now()->addHours( 2 ) ); $this->assertTrue( $event->isPublished() ); } } ``` The `travel()` method accepts various date formats, making it flexible for different testing scenarios: ```php use Mantle\Support\Carbon; // Using a Carbon instance $this->travel( Carbon::parse( '2024-01-15 10:00:00' ) ); // Using a DateTime instance $this->travel( new \DateTime( '2024-01-15 10:00:00' ) ); // Using a string (parsed by Carbon) $this->travel( '2024-01-15 10:00:00' ); // Using relative time with the now() helper $this->travel( now()->subDays( 7 ) ); ``` ##### Traveling for a Specific Operation[​](#traveling-for-a-specific-operation "Direct link to Traveling for a Specific Operation") Sometimes you only need to travel through time for a specific operation and then return to the current time. The `travel_to()` method allows you to travel to a specific time, execute a callback, and then automatically travel back to the current time: ```php use Mantle\Support\Carbon; class ExpirationTest extends TestCase { public function test_coupon_expiration(): void { $coupon = Coupon::create([ 'code' => 'SAVE20', 'expires_at' => now()->addDays( 7 ), ]); // Coupon should be valid now $this->assertTrue( $coupon->isValid() ); // Travel to 8 days in the future just for this assertion $this->travel_to( now()->addDays( 8 ), function () use ( $coupon ) { $this->assertFalse( $coupon->fresh()->isValid() ); } ); // We're back to the current time $this->assertTrue( $coupon->fresh()->isValid() ); } } ``` ##### Traveling Back to Current Time[​](#traveling-back-to-current-time "Direct link to Traveling Back to Current Time") While time changes are automatically cleaned up after each test, you can manually travel back to the current time using the `travel_back()` method: ```php class TimeTest extends TestCase { public function test_time_sensitive_feature(): void { // Travel to a specific time $this->travel( Carbon::parse( '2024-01-01 00:00:00' ) ); // Do some testing... $this->assertEquals( '2024-01-01', now()->format( 'Y-m-d' ) ); // Travel back to current time $this->travel_back(); // Now we're back to the current time $this->assertEquals( now()->format( 'Y-m-d' ), ( new \DateTime() )->format( 'Y-m-d' ) ); } } ``` ##### Working with the Carbon Class[​](#working-with-the-carbon-class "Direct link to Working with the Carbon Class") Mantle provides its own `Carbon` class that extends Carbon/Carbon with additional functionality for time mocking. You can use it anywhere in your tests to access time-related functionality: ```php use Mantle\Support\Carbon; class PostTest extends TestCase { public function test_post_scheduling(): void { $publishTime = Carbon::parse( '2024-06-15 14:00:00' ); $post = Post::create([ 'title' => 'Scheduled Post', 'publish_at' => $publishTime, ]); // Travel to just before publish time $this->travel( $publishTime->copy()->subMinute() ); $this->assertFalse( $post->isPublished() ); // Travel to publish time $this->travel( $publishTime ); $this->assertTrue( $post->isPublished() ); } } ``` For advanced use cases, you can set the test time directly using `Carbon::set_test_now()`: ```php use Mantle\Support\Carbon; class AdvancedTimeTest extends TestCase { public function test_custom_time_mocking(): void { // Set the test time directly Carbon::set_test_now( Carbon::parse( '2024-01-01 12:00:00' ) ); // All Carbon instances will now return this time $this->assertEquals( '2024-01-01 12:00:00', Carbon::now()->format( 'Y-m-d H:i:s' ) ); // Clear the test time Carbon::set_test_now( null ); } } ``` ##### Testing with the `now()` Helper[​](#testing-with-the-now-helper "Direct link to testing-with-the-now-helper") The global `now()` helper automatically respects time mocking, making it perfect for use throughout your application code: ```php use function Mantle\Support\Helpers\now; class EventTest extends TestCase { public function test_event_timing(): void { // Travel to a specific time $this->travel( '2024-03-15 09:00:00' ); // The now() helper returns the mocked time $event = Event::create([ 'name' => 'Conference', 'starts_at' => now(), 'ends_at' => now()->addHours( 3 ), ]); $this->assertEquals( '2024-03-15 09:00:00', $event->starts_at->format( 'Y-m-d H:i:s' ) ); $this->assertEquals( '2024-03-15 12:00:00', $event->ends_at->format( 'Y-m-d H:i:s' ) ); } } ``` --- ### Testing: Hooks #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides an interface for testing WordPress hooks in declarative and assertive formats. #### Declaring Hook Usage[​](#declaring-hook-usage "Direct link to Declaring Hook Usage") Inside your unit test you can declare the hook(s) you expect to fire and optionally specifying the amount of times and their return values. This is done via the `$this->expectApplied()` method. Once declared, you can then run the subsequent function that will apply the WordPress filter and Mantle will handle the assertions. ```php $this->expectApplied( 'action_to_check' ) ->twice() ->with( 'value_to_check', 'secondary_value_to_check' ); ``` ##### Defining Count[​](#defining-count "Direct link to Defining Count") Define how many times a hook was applied. You can specify the number of times directly with `times()` or use `once()`, `twice()`, or `never()` instead. ```php $this->expectApplied( 'action_to_check' )->once(); $this->expectApplied( 'action_to_check' )->twice(); $this->expectApplied( 'action_to_check' )->never(); // Perform the do_action() calls... ``` ##### Defining Arguments[​](#defining-arguments "Direct link to Defining Arguments") Define the arguments that you expect to be passed to the filter. These would be the arguments passed to `do_action()`/`apply_filters()`/etc. at the start of the hook. ```php $this->expectApplied( 'filter_to_check' ) ->once() ->with( 'value_to_check' ); apply_filters( 'filter_to_check', 'value_to_check' ); ``` ##### Defining Return Value[​](#defining-return-value "Direct link to Defining Return Value") Define the expected return value for the filter. Return values can be specified using `andReturn(mixed $value)` or with some helper functions. * `andReturn(mixed $value)`: Returns with the value of `$value`. * `andReturnNull()`: Returns `null`. * `andReturnTrue()`: Returns `true`. * `andReturnTruthy()`: Returns `true` if the value is truthy. * `andReturnFalse()`: Returns `false.` * `andReturnFalsy()`: Returns `false` if the value is falsy. * `andReturnEmpty()`: Returns `true` if the value is empty. * `andReturnNotEmpty()`: Returns `false` if the value is not empty. * `andReturnArray()`: Returns an array. * `andReturnInstanceOf( string $class )`: Returns an instance of `$class`. * `andReturnString()`: Returns a string. * `andReturnInteger()`: Returns an integer. ```php $this->expectApplied( 'falsey_filter_to_check' ) ->once() ->andReturnFalse(); add_filter( 'falsey_filter_to_check', '__return_false' ); apply_filters( 'falsey_filter_to_check', true ); ``` #### Declaring Hook Added (add\_action/add\_filter)[​](#declaring-hook-added-add_actionadd_filter "Direct link to Declaring Hook Added (add_action/add_filter)") You can use `expectAdded()` to declare that a hook was added to WordPress. This is useful for asserting that a hook was added with the correct arguments. ```php // Check that "action_to_check" has any action added to it. $this->expectAdded( 'action_to_check' ); // Check that "action_to_check" has a specific callable added to it. $this->expectAdded( 'action_to_check', '__return_true' ); ``` #### Asserting Hook Usage[​](#asserting-hook-usage "Direct link to Asserting Hook Usage") Hooks can be asserted against after they have already been applied. This can be done using the `$this->assertHookApplied()` method and can be used interchangeably with `expectApplied()`. ```php // Assert that 'the_hook' was applied twice. $this->assertHookApplied( 'the_hook', 2 ); // Assert that 'my_custom_hook' was not applied. $this->assertHookNotApplied( 'my_custom_hook' ); ``` --- ### Testing: Installation Manager #### Introduction[​](#introduction "Direct link to Introduction") The Installation Manager is a class used to manage the installation of WordPress. Mantle aims to remove the need to install any external dependencies such as core testing suites, WordPress VIP MU plugins, or other testing tools. The Installation Manager should be capable of handling all of that and letting you focus on writing tests. The only thing you should need to do to test your plugin/theme/site is run `composer phpunit`. #### Supported Use Cases[​](#supported-use-cases "Direct link to Supported Use Cases") The Installation Manager supports two main use cases: 1. Installing WordPress for testing in an existing WordPress installation. The existing installation's code will be used but the Installation Manager will handle setting up the database and other configuration needed for testing. The [Modifying the WordPress Installation](#modifying-the-wordpress-installation) section will cover how to customize the WordPress installation for your specific testing needs. 2. Installing WordPress for testing in a temporary directory and rsync-ing your project to live within it for testing. The [Rsync-ing](#rsync-ing) section will cover how to rsync your project for testing purposes and the [Modifying the WordPress Installation](#modifying-the-wordpress-installation) section will cover how to customize the WordPress installation for your specific testing needs after the rsync process. #### Modifying the WordPress Installation[​](#modifying-the-wordpress-installation "Direct link to Modifying the WordPress Installation") The Installation Manager supports fluent methods for modifying the WordPress installation before/after the installation process. It also has helpers to aid in the setup process for projects to make it easier to get testing. ##### Registering a Before Callback[​](#registering-a-before-callback "Direct link to Registering a Before Callback") Before callbacks are registered using the `before()` method. The callback will be executed before the WordPress installation is started. ```php \Mantle\Testing\manager() ->before( function() { // Do something before the installation. } ) ->install(); ``` ##### Registering an After Callback[​](#registering-an-after-callback "Direct link to Registering an After Callback") After callbacks are registered using the `after()` method. The callback will be executed after the WordPress installation is finished. ```php \Mantle\Testing\manager() ->after( function() { // Do something after the installation. } ) ->install(); ``` ##### Registering a Loaded Callback[​](#registering-a-loaded-callback "Direct link to Registering a Loaded Callback") Loaded callbacks are registered using the `loaded()` method. The callback will be executed after the WordPress installation is finished and during the `muplugins_loaded` WordPress hook. ```php \Mantle\Testing\manager() ->loaded( function() { // Do something after the installation such as loading // your plugin's main file. } ) ->install(); ``` ##### Enabling Multisite[​](#enabling-multisite "Direct link to Enabling Multisite") Multisite can be enabled using the `with_multisite()` method. This will configure WordPress to use Multisite during the installation process. ```php \Mantle\Testing\manager() ->with_multisite() ->install(); ``` ##### Registering a `init` Callback[​](#registering-a-init-callback "Direct link to registering-a-init-callback") Callbacks can be registered to run during the `init` WordPress hook using the `init()` method. ```php \Mantle\Testing\manager() ->init( function() { // Do something during the init hook. } ) ->install(); ``` ##### Changing the Active Theme[​](#changing-the-active-theme "Direct link to Changing the Active Theme") The active theme can be changed using the `with_theme()` method. The method accepts the theme name and will switch to the active theme on the `muplugins_loaded` hook: ```php \Mantle\Testing\manager() ->with_theme( 'my-theme-name' ) ->install(); ``` Calling `with_theme()` will also set the `WP_DEFAULT_THEME` environmental variable to the theme. ##### Changing the Active Plugins[​](#changing-the-active-plugins "Direct link to Changing the Active Plugins") The active plugins can be changed using the `with_plugins()` method. The method accepts an array of plugin file paths (mirrors the `active_plugins` option) and will switch to the active plugins on the `muplugins_loaded` hook: ```php \Mantle\Testing\manager() ->with_plugins( [ 'my-plugin/my-plugin.php', 'my-other-plugin/my-other-plugin.php', ] ) ->install(); ``` ##### Changing an Option[​](#changing-an-option "Direct link to Changing an Option") The `with_option()` method can be used to set an option in the WordPress database. The method accepts the option name and value: ```php \Mantle\Testing\manager() ->with_option( 'my_option', 'my_value' ) ->install(); ``` ##### Changing the Site/Home URL[​](#changing-the-sitehome-url "Direct link to Changing the Site/Home URL") The `with_url()` method can be used to set the site and home URLs in the WordPress. Both are optional. ```php \Mantle\Testing\manager() ->with_url( home: 'https://example.com', site: 'https://example.com' ) ->install(); ``` ##### Using the Experimental Feature for Home URL in Testing[​](#using-the-experimental-feature-for-home-url-in-testing "Direct link to Using the Experimental Feature for Home URL in Testing") Mantle is switching to use the site's home URL for testing as the request host rather than the hard-coded value of the `WP_TESTS_DOMAIN` constant. This is a more accurate representation of how WordPress is used in production. In the next major version of Mantle, this will be the default behavior. To enable this feature, you can use the `with_experimental_testing_url_host()` method: ```php \Mantle\Testing\manager() ->with_experimental_testing_url_host() ->install(); ``` You can customize the [site and home URLs](#changing-the-sitehome-url) via the Installation Manger. Heads up! This feature will be made the default behavior in the next major version of Mantle. Enable via a Environmental Variable You can also set the default behavior by setting the `MANTLE_EXPERIMENTAL_TESTING_USE_HOME_URL_HOST` environmental variable to `true`. ##### Enabling Debug Mode[​](#enabling-debug-mode "Direct link to Enabling Debug Mode") Debug mode can be enabled using the `with_debug()` method. This will configure the Installation Manager to set the `MANTLE_TESTING_DEBUG` environmental variable to `'1'` during the installation process. You will see more verbose output from the installation script. This can be helpful when debugging issues with the installation or rsync process. ```php \Mantle\Testing\manager() ->with_debug() ->install(); ``` #### Rsync-ing[​](#rsync-ing "Direct link to Rsync-ing") The Installation Manager can manage the entire rsync process without you needing to manually execute a shell script before running your unit tests. ##### Overriding the Default Installation[​](#overriding-the-default-installation "Direct link to Overriding the Default Installation") By default, Mantle will install WordPress to a temporary directory. This will default to `/tmp/wordpress` or another temporary directory if that isn't available. The default installation will also make some assumptions about the default configuration of WordPress, such as the database name, username, and password. The following are the default values and the environmental variables that can be used to override them: | Variable/Setting | Default Value | Environmental Variable Name | | ---------------------- | ---------------------- | --------------------------- | | Installation Path | `/tmp/wordpress` | `WP_CORE_DIR` | | `DB_NAME` | `wordpress_unit_tests` | `WP_DB_NAME` | | `DB_USER` | `root` | `WP_DB_USER` | | `DB_PASSWORD` | `root` | `WP_DB_PASSWORD` | | `DB_HOST` | `localhost` | `WP_DB_HOST` | | WordPress Version | `latest` | `WP_VERSION` | | Skip Database Creation | `false` | `WP_SKIP_DB_CREATE` | | PHPUnit Path | `vendor/bin/phpunit` | `WP_PHPUNIT_PATH` | Mantle uses `getenv()` to retrieve the environmental variables and will fallback to the default values if the environmental variable isn't set. ##### Rsync-ing a `wp-content/`-rooted Project[​](#rsync-ing-a-wp-content-rooted-project "Direct link to rsync-ing-a-wp-content-rooted-project") If you're working on a project that is rooted in the `wp-content/` directory (a WordPress VIP site for example), you can use the `maybe_rsync()` to rsync the entire project over to a working WordPress installation. ```php \Mantle\Testing\manager() ->maybe_rsync() ->install(); ``` Your project will then be rsynced to the `wp-content/` directory within the WordPress installation. ##### Rsync-ing a Plugin[​](#rsync-ing-a-plugin "Direct link to Rsync-ing a Plugin") Plugins can be rsync'd to live within a WordPress installation by using the `maybe_rsync_plugin()`. Within your `tests/bootstrap.php` file, you can use the following code: ```php \Mantle\Testing\manager() ->maybe_rsync_plugin() ->install(); ``` By default, the Installation Manager will set the default name of your plugin. If you'd like to customize it, you can pass the name to the `maybe_rsync_plugin()` method: ```php \Mantle\Testing\manager() ->maybe_rsync_plugin( 'my-plugin-name' ) ->install(); ``` The installation manager will place the plugin in the `wp-content/plugins/` directory within the WordPress installation. note Plugins normally don't need to be rsync'd. Mantle will automatically install WordPress if not found and use the installation without rsyncing the plugin. ##### Rsync-ing a Theme[​](#rsync-ing-a-theme "Direct link to Rsync-ing a Theme") Themes can be rsynced to live within a WordPress installation by using the `maybe_rsync_theme()`. Within your `tests/bootstrap.php` file, you can use the following code: ```php Mantle\Testing\manager() ->maybe_rsync_theme() ->install(); ``` By default, the Installation Manager will set the default name of the theme. If you'd like to customize it, you can pass the name to the `maybe_rsync_theme()` method: ```php Mantle\Testing\manager() ->maybe_rsync_theme( 'my-theme-name' ) ->install(); ``` The installation manager will place the theme in the `wp-content/themes/` directory within the WordPress installation. ##### Including WordPress VIP MU Plugins[​](#including-wordpress-vip-mu-plugins "Direct link to Including WordPress VIP MU Plugins") Mantle can automatically install the built-version of [WordPress VIP's MU Plugins](https://github.com/Automattic/vip-go-mu-plugins-built) to your testing installation. ```php Mantle\Testing\manager() ->maybe_rsync_wp_content() ->with_vip_mu_plugins() ->install(); ``` If your project does include a `mu-plugins` folder, it will be ignored and will not be rsync'd to the testing installation. You can also set the `MANTLE_INSTALL_VIP_MU_PLUGINS` environmental variable to `true` to automatically include the WordPress VIP MU Plugins in your testing installation. ##### Including Memcache Object Cache Drop-In[​](#including-memcache-object-cache-drop-in "Direct link to Including Memcache Object Cache Drop-In") If your project uses the [Memcache Object Cache Drop-In](https://raw.githubusercontent.com/Automattic/wp-memcached/HEAD/object-cache.php), you can include it in your testing installation for parity with your production environment. ```php Mantle\Testing\manager() ->maybe_rsync_wp_content() ->with_object_cache() ->install(); ``` You can also set the `MANTLE_INSTALL_OBJECT_CACHE` environmental variable to `true` to automatically include the Memcache Object Cache Drop-In in your testing installation. note If your testing environment does not include the `Memcache` extension, the Memcache Object Cache Drop-In will not be installed to prevent a fatal error. If you want to force the installation of the Memcache Object Cache Drop-In, you set the `MANTLE_REQUIRE_OBJECT_CACHE` environmental variable to `true`. ##### Including Redis Object Cache Drop-In[​](#including-redis-object-cache-drop-in "Direct link to Including Redis Object Cache Drop-In") If your project uses Redis for object caching, you can include the [wp-redis object-cache.php](https://github.com/pantheon-systems/wp-redis/blob/HEAD/object-cache.php) file instead. ```php Mantle\Testing\manager() ->maybe_rsync_wp_content() ->with_object_cache( 'redis' ) ->install(); ``` You can also set the `MANTLE_INSTALL_OBJECT_CACHE` environmental variable to `redis` and Mantle will automatically include the Redis Object Cache Drop-In for you. ##### Installing Additional Plugins[​](#installing-additional-plugins "Direct link to Installing Additional Plugins") If you'd like to install additional plugins to the testing installation, you can use the `install_plugin()` method. This method accepts a string of the plugin name and a version or URL to a zip file. ```php \Mantle\Testing\manager() // Install the plugins after rsyncing to WP_CORE_DIR. ->install_plugin( 'akismet' ) ->install_plugin( 'byline-manager', 'https://github.com/alleyinteractive/byline-manager/archive/refs/heads/production.zip' ) // Tell WordPress to activate the plugins. ->plugins( [ 'akismet/akismet.php', 'byline-manager/byline-manager.php', ] ) ->install(); ``` ##### Using SQLite for the Database[​](#using-sqlite-for-the-database "Direct link to Using SQLite for the Database") Mantle can automatically configure WordPress to use SQLite for the database instead of MySQL for testing. This can be a big leap in performance for your average project. This is powered by the [db.php drop-in](https://github.com/aaemnnosttv/wp-sqlite-db). To enable SQLite, you can use the `with_sqlite()` method: ```php \Mantle\Testing\manager() ->with_sqlite() ->install(); ``` You can also set the `MANTLE_USE_SQLITE` environmental variable to `true` to use SQLite for testing by default. By default, Mantle will use MySQL for the database. SQLite will work well for most use-cases but there are some limitations. For example, if you're creating database tables or performing complex SQL queries, you may run into issues and are better off not using SQLite. ##### Disabling Object Cache for Local Development[​](#disabling-object-cache-for-local-development "Direct link to Disabling Object Cache for Local Development") If you're working on a local development environment and don't want to use an object cache when testing, you can use the `without_local_object_cache()` method: ```php \Mantle\Testing\manager() ->without_local_object_cache() ->install(); ``` ##### Excluding Files from Rsync[​](#excluding-files-from-rsync "Direct link to Excluding Files from Rsync") If you'd like to exclude files from being rsync'd to the testing installation, you can use the `exclusions()` method. This method accepts an array of files that will be passed to `rsync` and be excluded from the rsync process. ```php \Mantle\Testing\manager() ->exclusions( [ 'vendor/', 'node_modules/', ] ) ->install(); ``` #### Performing the Installation[​](#performing-the-installation "Direct link to Performing the Installation") The installation process can be started by calling the `install()` method on the Installation Manager. ```php \Mantle\Testing\manager()->install(); // Or, if you're using the helper function. \Mantle\Testing\install(); ``` #### About the Installation Script[​](#about-the-installation-script "Direct link to About the Installation Script") The Installation Manager uses a installation script located in the [mantle-ci repository](https://github.com/alleyinteractive/mantle-ci/blob/HEAD/install-wp-tests.sh). The script provides a fast way of installing WordPress for testing purposes. It also supports caching the installation between runs to speed up the process. For more information, see the documentation in the script. --- ### Migrating to PHPUnit 10+ from 9 Mantle's Test Framework 1.0 upgrades the PHPUnit version to 10.x. This is a major upgrade and may require some changes to your existing tests. The most common change is the need for projects to use PSR-4 file/class naming conventions. This is a breaking change from PHPUnit 9.x that allowed for classic WordPress-style file/class naming conventions in your tests. In Mantle 1.14, we dropped support for PHPUnit 9.x and now require PHPUnit 10.x for all tests. If you need to continue using PHPUnit 9.x, you can stay on Mantle 1.13 or earlier. When you are ready to upgrade to Mantle 1.14 or later, you will need to migrate your tests to use PSR-4 file/class naming conventions with PHPUnit 10+. Migration to PHPUnit 10+ is generally straightforward. You may use [`alleyinteractive/wp-to-psr-4`](https://github.com/alleyinteractive/wp-to-psr-4/) to help automate the process of converting your test files to PSR-4 naming conventions. For more information, see the [1.x CHANGELOG note about the PHPUnit 10 Migration](https://github.com/alleyinteractive/mantle-framework/blob/1.x/CHANGELOG.md#phpunit-10-migration) for more information. --- ### Testing: Parallel Testing note Parallel unit testing is currently in beta testing. Please let us know any issues you may come across. It is generally a drop-in replacement for PHPUnit but does play better with PHPUnit 10+. By default, PHPUnit runs tests sequentially. This can be slow, especially if you have a lot of tests. To speed up your test suite, you can run tests in parallel. To get started, you'll need to install the `paratest` package: ```bash composer require --dev brianium/paratest ``` You can then call the `vendor/bin/paratest` binary to run your tests in parallel: ```bash vendor/bin/paratest ``` You should also update your `composer.json` file to include a script to run your tests in parallel: ```json { "scripts": { "test": "vendor/bin/paratest" } } ``` --- ### Testing: Pest Mantle's Testing Framework supports running unit tests via Pest via the [alleyinteractive/pest-plugin-wordpress plugin](https://github.com/alleyinteractive/pest-plugin-wordpress). Pest tests using Mantle can be run with or without the rest of the framework. ![Pest Logo](https://pestphp.com/assets/img/pestinstall.png) Let's take a look at a quick example of a Pest test case using Mantle: ```php use function Pest\PestPluginWordPress\from; use function Pest\PestPluginWordPress\get; it( 'should load the homepage', function () { get( '/' ) ->assertStatus( 200 ) ->assertSee( 'home' ); } ); it( 'should load with a referrer', function () { from( 'https://laravel.com/' ) ->get( '/' ) ->assertStatus( 200 ); }); ``` For more information, [checkout the plugin's GitHub](https://github.com/alleyinteractive/pest-plugin-wordpress#getting-started). --- ### Testing: Remote Requests #### Introduction[​](#introduction "Direct link to Introduction") Remote request mocks are a very common use case to test against when unit testing. Mantle gives you the ability to mock specific requests and fluently generate a response. By default Mantle won't mock any HTTP request but will actively notify you when one is being made inside a unit test. To prevent any non-mocked requests from being made, see [preventing stray requests](#preventing-stray-requests). note This only supports requests made via the [`WP_Http` API](https://developer.wordpress.org/reference/functions/wp_remote_request/) (`wp_remote_request()`, `wp_remote_get()`, `wp_remote_post()`, etc.) #### Faking Requests[​](#faking-requests "Direct link to Faking Requests") Intercept all remote requests with a specified response code and body using the `$this->fake_request()` method. ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request() ->with_response_code( 404 ) ->with_body( 'test body' ); // You can now make a remote request and it will return a 404. $response = wp_remote_get( 'https://example.com/' ); } } ``` The `$this->fake_request()` method returns a `Mantle\Testing\Mock_Http_Response` object that can allow you to fluently build a response. See [generating responses](#generating-responses) for more information about building a response. ##### Faking a Specific Endpoint[​](#faking-a-specific-endpoint "Direct link to Faking a Specific Endpoint") You can specify a specific endpoint to fake by passing a URL to the `$this->fake_request()` method. ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( 'https://example.com/path' ) ->with_response_code( 404 ) ->with_body( 'test body' ); // You can now make a remote request to `https://example.com/path` and it will // return a 404. $response = wp_remote_get( 'https://example.com/path' ); } } ``` You can also use wildcards in the URL to match multiple endpoints. The following example will match any request to `https://example.com/*`. ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( 'https://example.com/*' ) ->with_response_code( 404 ) ->with_body( 'test body' ); // You can now make a remote request to `https://example.com/` and it will // return a 404. $response = wp_remote_get( 'https://example.com/test' ); } } ``` ##### Faking Multiple Endpoints[​](#faking-multiple-endpoints "Direct link to Faking Multiple Endpoints") Faking a specific endpoint, `testing.com` will return a 404 while `github.com` will return a 500. ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( 'https://testing.com/*' ) ->with_response_code( 404 ) ->with_body( 'test body' ); $this->fake_request( 'https://github.com/*' ) ->with_response_code( 500 ) ->with_body( 'fake body' ); } } ``` Depending on your preferred style, you can also pass an array of URLs and responses to be faked using the `mock_http_response()` helper: ```php namespace App\Tests; use function Mantle\Testing\mock_http_response; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( [ 'https://github.com/*' => mock_http_response()->with_body( 'github' ), 'https://twitter.com/*' => mock_http_response()->with_body( 'twitter' ), ] ); } } ``` ##### Faking With a Callback[​](#faking-with-a-callback "Direct link to Faking With a Callback") If you require more complicated logic to determine the response, you can use a closure that will be invoked when a request is being faked. It should return a mocked HTTP response: ```php namespace App\Tests; use function Mantle\Testing\mock_http_response; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( function( string $url, array $request_args ) { if ( false === strpos( $url, 'alley.co' ) ) { return; } return $this->mock_response() ->with_response_code( 123 ) ->with_body( 'alley!' ); } ); } } ``` The `fake_request()` method also supports typehinting a `Mantle\Http_Client\Request` object as the first argument, which allows you to use the request object to determine the response: ```php namespace App\Tests; use Mantle\Http_Client\Request; use function Mantle\Testing\mock_http_response; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( function( Request $request ) { if ( 'alley.com' !== $request->host() ) { return; } return mock_http_response() ->with_response_code( 123 ) ->with_body( 'alley!' ); } ); } } ``` If the callback returns an empty value (null/false), the request will not be faked and the next matching fake will be used, or an actual request will be made if no matching fake is found. ##### Faking Response Sequences[​](#faking-response-sequences "Direct link to Faking Response Sequences") Sometimes a single URL should return a series of fake responses in a specific order. This can be accomplished via `Mantle\Testing\Mock_Http_Sequence` class and `mock_http_sequence` helper to build the response sequence. In the following example, the first request to `github.com` will return a 200, the second a 400, and the third a 500: ```php namespace App\Tests; use function Mantle\Testing\mock_http_sequence; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( [ 'https://github.com/*' => mock_http_sequence() // Push a status code. ->push_status( 200 ) // Push a JSON response. ->push_json( [ 1, 2, 3 ] ) // Push a response with a body. ->push_body( 'test body' ) // Push a entire response object. ->push( mock_http_response()->with_status( 204 ) ) ], ); } } ``` Any request made in the above example will use a response in the provided sequence. There are various helpers such as `push_status`, `push_json`, `push_body`, etc. that can be used to help create a response. You can also pass a `Mantle\Testing\Mock_Http_Response` object to the `push` method to push a specific response. When all the responses in a sequence have been consumed, further requests will throw an exception because there are no remaining responses that can be returned. You can also specify a default response that will be returned when there are no responses left in the sequence: ```php namespace App\Tests; use function Mantle\Testing\mock_http_response; use function Mantle\Testing\mock_http_sequence; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( [ 'https://github.com/*' => mock_http_sequence() ->push_status( 200 ) ->push_status( 400 ) ->push_status( 500 ) ->when_empty( mock_http_response()->with_json( [ 4, 5, 6 ] ) ], ); } } ``` ##### Faking a Specific HTTP Method[​](#faking-a-specific-http-method "Direct link to Faking a Specific HTTP Method") By default, a HTTP request will be faked regardless of the method used. If you want to fake a request only when a specific method is used, you can pass the `method` argument to the `$this->fake_request()` method: ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->fake_request( 'https://example.com/', method: 'GET' ) ->with_response_code( 200 ) ->with_body( 'test body' ); $this->fake_request( 'https://example.com/', method: 'POST' ) ->with_response_code( 201 ) ->with_body( 'test created' ); // You can now make a remote request and it will return the first fake. $response = wp_remote_get( 'https://example.com/' ); // You can now make a remote request and it will return the second fake. $response = wp_remote_post( 'https://example.com/' ); } } ``` ##### Snapshot Testing Remote Requests[​](#snapshot-testing-remote-requests "Direct link to Snapshot Testing Remote Requests") Mantle provides the ability to use [snapshot testing](/docs/testing/snapshot-testing.md) with HTTP request mocking. This allows you to record the response of a real remote request and use it for mocking a request instead of having to manually create the response. This is especially useful when working with external APIs where you want to test against real responses without making actual HTTP requests during your test suite runs. The snapshot is placed in the `__http_snapshots__` directory relative to the test file. ```php namespace App\Tests; use Tests\Test_Case; class Example_Test extends Test_Case { public function test_snapshot_testing() { // This will create a snapshot of the response from 'https://alley.com/wp-json/*' // The first time the test runs, it will record the response. // On subsequent runs, it will use the recorded snapshot. $this->fake_request( 'https://alley.com/wp-json/*' )->with_snapshot(); // Make the request that will use the snapshot $response = wp_remote_get( 'https://alley.com/wp-json/wp/v2/posts' ); } } ``` ###### Customizing Snapshot IDs[​](#customizing-snapshot-ids "Direct link to Customizing Snapshot IDs") By default, Mantle generates a unique snapshot ID based on the test name, HTTP method, URL, and a hash of the request headers and body. You can customize the snapshot ID if needed: ```php namespace App\Tests; use Tests\Test_Case; class Example_Test extends Test_Case { public function test_custom_snapshot_id() { $this->fake_request( 'https://api.example.com/posts' ) ->with_snapshot( 'custom-snapshot-name' ); $response = wp_remote_get( 'https://api.example.com/posts' ); } } ``` ###### Updating Snapshots[​](#updating-snapshots "Direct link to Updating Snapshots") When an API response changes and you want to update your snapshots, you can run your tests with the `--update-snapshots` flag: ```bash ./vendor/bin/phpunit -d --update-snapshots ``` This will update all the snapshots used in your tests to match the current responses ##### Generating Responses[​](#generating-responses "Direct link to Generating Responses") `Mantle\Testing\Mock_Http_Response` class, and the `$this->mock_response()`/`mock_http_response()` helpers exists to help you fluently build a mock remote response. The following methods are available to build a response and can be chained together: ###### `with_status( int $status )` / `with_response_code( int $status )`[​](#with_status-int-status---with_response_code-int-status- "Direct link to with_status-int-status---with_response_code-int-status-") Create a response with a specific status code. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_status( 200 ); ``` ###### `with_body( string $body )`[​](#with_body-string-body- "Direct link to with_body-string-body-") Create a response with a specific body. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_body( 'test body' ); ``` ###### `with_json( $payload )`[​](#with_json-payload- "Direct link to with_json-payload-") Create a response with a JSON body and set the `Content-Type` header to `application/json`. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_json( [ 1, 2, 3 ] ); ``` You can also pass an array to the `$response` argument of `fake_request()` to create a response with a JSON body: ```php use function Mantle\Testing\mock_http_response; // A request to `https://example.com/` will return a JSON response with the body `[1,2,3]`. $this->fake_request( 'https://example.com/', [ 1, 2, 3 ] ); ``` ###### `with_xml( string $payload )`[​](#with_xml-string-payload- "Direct link to with_xml-string-payload-") Create a response with an XML body and set the `Content-Type` header to `application/xml`. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_xml( '' ); ``` ###### `with_header( string $key, string $value )`[​](#with_header-string-key-string-value- "Direct link to with_header-string-key-string-value-") Create a response with a specific header. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_header( 'Content-Type', 'application/json' ); ``` ###### `with_headers( array $headers )`[​](#with_headers-array-headers- "Direct link to with_headers-array-headers-") Create a response with specific headers. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_headers( [ 'Content-Type' => 'application/json' ] ); ``` ###### `with_cookie( \WP_Http_Cookie $cookie )`[​](#with_cookie-wp_http_cookie-cookie- "Direct link to with_cookie-wp_http_cookie-cookie-") Create a response with a specific cookie. ```php use function Mantle\Testing\mock_http_response; $cookie = new \WP_Http_Cookie(); $cookie->name = 'test'; $cookie->value = 'value'; mock_http_response()->with_cookie( $cookie ); ``` ###### `with_redirect( string $url, int $code = 301 )`[​](#with_redirect-string-url-int-code--301- "Direct link to with_redirect-string-url-int-code--301-") Create a response with a specific redirect. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_redirect( 'https://wordpress.org/' ); ``` ###### `with_temporary_redirect( string $url )`[​](#with_temporary_redirect-string-url- "Direct link to with_temporary_redirect-string-url-") Create a response with a specific temporary redirect. ```php use function Mantle\Testing\mock_http_response; mock_http_response()->with_temporary_redirect( 'https://wordpress.org/' ); ``` ###### `with_file( string $path )`[​](#with_file-string-path- "Direct link to with_file-string-path-") Create a response with a file as the response body. The appropriate `Content-Type` header will be set based on the file extension. ```php use function Mantle\Testing\mock_http_response; $this->fake_request( 'https://example.com/file' ) ->with_file( '/path/to/file' ); // Use the `mock_http_response` helper if you'd like. mock_http_response()->with_file( '/path/to/file' ); ``` ###### `with_filename( string $filename )`[​](#with_filename-string-filename- "Direct link to with_filename-string-filename-") Create a response with a specific filename. ```php use function Mantle\Testing\mock_http_response; $this->fake_request( 'https://example.com/file' ) ->with_filename( 'test.txt' ); // Use the `mock_http_response` helper if you'd like. mock_http_response()->with_filename( 'test.txt' ); ``` ###### `with_image( ?string $filename = null )`[​](#with_image-string-filename--null- "Direct link to with_image-string-filename--null-") Create a response with an image as the response body. The image will be a JPEG image by default. ```php use function Mantle\Testing\mock_http_response; $this->fake_request( 'https://example.com/image' ) ->with_image(); // Use the `mock_http_response` helper if you'd like. mock_http_response()->with_image(); ``` #### Asserting Requests[​](#asserting-requests "Direct link to Asserting Requests") All remote requests can be asserted against, even if they're not being faked by the test case. Mantle will always log if an actual remote request is being made during a unit test. ##### Assertions[​](#assertions "Direct link to Assertions") ###### `assertRequestSent( string|callable|null $url_or_callback = null, int $expected_times = null )`[​](#assertrequestsent-stringcallablenull-url_or_callback--null-int-expected_times--null- "Direct link to assertrequestsent-stringcallablenull-url_or_callback--null-int-expected_times--null-") Assert that a request was sent to a specific URL. ```php namespace App\Tests; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { Http::get( 'https://example.com/' ); $this->assertRequestSent( 'https://example.com/' ); } } ``` Sometimes you may want to assert that a request was sent with specific mix of headers/methods/body requirements. This can be done using a callback that is passed the `Mantle\Http_Client\Request` object: ```php use Mantle\Facade\Http; use Mantle\Http_Client\Request; use Tests\Test_Case; class Example_Test extends Test_Case { public function test_example() { Http::with_basic_auth( 'user', 'pass' ) ->get( 'https://example.com/basic-auth/' ); // Assert that a request was sent with an authorization header, // is a GET request and to a specific URL. $this->assertRequestSent( fn ( Request $request ) => $request ->has_header( 'Authorization', 'Basic dXNlcjpwYXNz' ) && 'https://example.com/basic-auth/' === $request->url() && 'GET' === $request->method() ); // Assert that a request was not sent with a specific header, $this->assertRequestNotSent( fn ( Request $request ) => $request ->has_header( 'Content-Type', 'application-json' ) && 'https://example.com/get/' === $request->url() && 'GET' === $request->method() ); } } ``` ###### `assertRequestNotSent( string|callable|null $url_or_callback = null )`[​](#assertrequestnotsent-stringcallablenull-url_or_callback--null- "Direct link to assertrequestnotsent-stringcallablenull-url_or_callback--null-") Assert that a request was not sent to a specific URL. ```php namespace App\Tests; use Tests\Test_Case; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { Http::get( 'https://example.com/' ); $this->assertRequestNotSent( 'https://example.com/not-sent/' ); } } ``` ###### `assertNoRequestSent()`[​](#assertnorequestsent "Direct link to assertnorequestsent") Assert that no requests were sent. ```php namespace App\Tests; use Tests\Test_Case; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { $this->assertNoRequestSent(); } } ``` ###### `assertRequestCount( int $number )`[​](#assertrequestcount-int-number- "Direct link to assertrequestcount-int-number-") Assert that a specific number of requests were sent. ```php namespace App\Tests; use Tests\Test_Case; class ExampleRequestTest extends Test_Case { /** * Example test. */ public function test_example() { Http::get( 'https://example.com/' ); $this->assertRequestCount( 1 ); } } ``` #### Preventing Stray Requests[​](#preventing-stray-requests "Direct link to Preventing Stray Requests") If you would like to ensure that all requests are faked during a unit test, you can use the `$this->prevent_stray_requests()` method. This will throw an `RuntimeException` instance if any requests are made that do not have a corresponding fake. ```php namespace App\Tests; use Tests\Test_Case; class Example_Test extends Test_Case { protected function setUp(): void { parent::setUp(); $this->prevent_stray_requests(); } public function test_example() { $this->fake_request( 'https://alley.com/*' ) ->with_response_code( 200 ) ->with_body( 'alley!' ); // An 'alley!' response is returned. Http::get( 'https://alley.com/' ); // An exception is thrown because the request was not faked. Http::get( 'https://github.com/' ); } } ``` Stray requests can be re-enabled by calling `$this->allow_stray_requests()`. ```php namespace App\Tests; use Tests\Test_Case; class Example_Test extends Test_Case { protected function setUp(): void { parent::setUp(); $this->prevent_stray_requests(); } public function test_example() { $this->fake_request( 'https://alley.com/*' ) ->with_response_code( 200 ) ->with_body( 'alley!' ); Http::get( 'https://alley.com/' ); $this->allow_stray_requests(); // An exception is not thrown because stray requests are allowed. Http::get( 'https://github.com/' ); } } ``` You can also use the `Mantle\Testing\Concerns\Prevent_Remote_Requests` trait to prevent stray requests in all tests in a test case. ```php namespace App\Tests; use Tests\Test_Case; use Mantle\Testing\Concerns\Prevent_Remote_Requests; class Example_Test extends Test_Case { use Prevent_Remote_Requests; public function test_example() { $this->fake_request( 'https://alley.com/*' ) ->with_response_code( 200 ) ->with_body( 'alley!' ); // An 'alley!' response is returned. Http::get( 'https://alley.com/' ); // An exception is thrown because the request was not faked. Http::get( 'https://github.com/' ); } } ``` #### Ignoring Stray Requests[​](#ignoring-stray-requests "Direct link to Ignoring Stray Requests") There may be cases where you want to ignore stray requests for a specific test while still preventing them in other tests. You can use the `$this->ignore_stray_requests()` method to ignore any stray requests matching a specific URL pattern. ```php namespace App\Tests; use Mantle\Testing\Concerns\Prevent_Remote_Requests; use Tests\Test_Case; class Example_Test extends Test_Case { use Prevent_Remote_Requests; public function test_example() { $this->ignore_stray_request( 'https://alley.com/*' ); $this->fake_request( 'https://alley.com/*' ) ->with_response_code( 200 ) ->with_body( 'alley!' ); } } ``` --- ### Testing: HTTP Tests #### Introduction[​](#introduction "Direct link to Introduction") Mantle provides a fluent HTTP Request interface to make it easier to write feature/integration tests using PHPUnit and WordPress. This library is a derivative work of Laravel's testing framework, customized to work with WordPress. In short, this library allows one to mimic a request to WordPress, and it sets up WordPress' global state as if it were handling that request (e.g. sets up superglobals and other WordPress-specific globals, sets up and executes the main query, loads the appropriate template file, etc.). It then creates a new `Test_Response` object which stores the details of the HTTP response, including headers and body content. The response object allows the developer to make assertions against the observable response data, for instance asserting that some content is present in the response body or that some header was set. For example, here's a simple test that asserts that a given post's title is present on the page: ```php namespace Tests\Feature; use Tests\TestCase; class Example_Test extends TestCase { /** * A basic test example. * * @return void */ public function test_basic_test() { $post = static::factory()->post->create_and_get( [ 'post_title' => 'Hello World', ] ); $this->get( $post->permalink() ) ->assertOk() ->assertSee( 'Hello World' ); } } ``` The `get` method makes a `GET` requests to the given URI, and the `assertSee` method asserts that the given string is present in the response body. All HTTP methods are available to use including `get`, `post`, `put`, `patch`, `delete` and `options`. #### Making Requests[​](#making-requests "Direct link to Making Requests") As a basic example, here we create a post via its factory, then request it and assert we see the post's name (title): ```php $post = static::factory()->post->create_and_get(); $this->get( $post ) ->assertSee( $post->post_title ); ``` In this example, here we request a non-existing resource and assert that it yields a 404 Not Found response: ```php $this->get( '/this/resource/does/not/exist/' ) ->assertNotFound(); ``` Lastly, here's an example of POSTing some data to an endpoint, and after following a redirect, asserting that it sees a success message: ```php $this->following_redirects() ->post( '/profile/', [ 'some_data' => 'hello' ] ) ->assertSee( 'Success!' ); ``` ##### Request Cookies[​](#request-cookies "Direct link to Request Cookies") Requests can have cookies included with them using the `with_cookie` and `with_cookies` methods: ```php $this->with_cookie( 'session', 'cookie-value' )->get( '/endpoint' ); // Pass multiple cookies. $this->with_cookies( [ 'key' => 'value', 'another' => 'value', ] ) ->get( '/example' ); ``` ##### Request Headers[​](#request-headers "Direct link to Request Headers") Request headers can be set for requests using the `with_header` and `with_headers` methods: ```php $this->with_header( 'api-key', '' )->get( '/example' ); $this->with_headers( [ 'API-Key' => '', 'X-Nonce' => 'nonce', ] ) ->get( '/example' ); ``` The `with_header`/`with_headers` methods can be used to set headers for any request but will only be used for that request you are chaining them to. If you want to set headers for all requests, you can use the `add_default_header` method to set a default header that will be used for all requests: ```php $this->add_default_header( 'X-Header-Name', 'header-value' ); $this->add_default_header( [ 'X-Header-Name' => 'header-value', 'X-Another-Header' => 'another-value', ] ); ``` You can remove the default headers using the `flush_default_headers` method: ```php $this->flush_default_headers(); ``` ##### Request Referrer[​](#request-referrer "Direct link to Request Referrer") The request referrer can be passed using the `from` method: ```php $this->from( 'https://wordpress.org/' )->get( '/example' ); ``` ##### ✨ Experimental ✨: Request Hosts[​](#-experimental--request-hosts "Direct link to ✨ Experimental ✨: Request Hosts") By default, core and Mantle's testing framework will use the `WP_TESTS_DOMAIN` constant as the host for all requests. There is an experimental feature that allows you to set the host relative to the value of the `home_url()` function/`home` option. This can be useful to make the request match the home URL you have setup in your test environment. To enable this feature, you can set the `MANTLE_EXPERIMENTAL_TESTING_USE_HOME_URL_HOST` environment variable to `true` or you can call the `with_experimental_testing_url_host()` method on the [installation manager](/docs/testing/installation-manager.md#using-the-experimental-feature-for-home-url-in-testing) in your bootstrap file. Example of a test that would previously have failed: ```php class Example_Test extends TestCase { public function test_example() { update_option( 'home', 'https://alley.com' ); $this->get( '/about/' ); // Without this feature flag, the HTTP_HOST would be `example.org'. $this->assertEquals( 'alley.com', $_SERVER['HTTP_HOST'] ); $this->assertEquals( 'on', $_SERVER['HTTPS'] ); } } ``` ##### HTTPS Request[​](#https-request "Direct link to HTTPS Request") Requests will be made "unsecure" by default. The means that the `HTTPS` server variable will not be set. You can force the request to be made over HTTPS by using the `with_https` method: ```php $this->with_https()->get( '/example' ); ``` You can also make all requests use HTTPS by setting the site's home URL to include `https://` and opt-in to the experimental feature to use the home URL host as the request host when testing. ##### Fetching Posts[​](#fetching-posts "Direct link to Fetching Posts") You can also use the `fetch_post()` method which combines post creation and HTTP request in one step. This method creates a post with the given attributes and then makes a GET request to its permalink: ```php $this->fetch_post( [ 'post_title' => 'Hello World', 'post_content' => 'This is the content.', ] ) ->assertOk() ->assertSee( 'Hello World' ); ``` This is equivalent to: ```php $post = static::factory()->post->create_and_get( [ 'post_title' => 'Hello World', 'post_content' => 'This is the content.', ] ); $this->get( $post->permalink() ) ->assertOk() ->assertSee( 'Hello World' ); ``` #### Testing Responses[​](#testing-responses "Direct link to Testing Responses") After making a request, you can make assertions against the response using the `Mantle\Testing\Test_Response` object returned by the request methods. The class has various methods to make assertions against the response with some more specific to the type of response (HTML, JSON, etc.). ```php namespace Tests\Feature; use Tests\TestCase; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function test_basic_test() { $post = static::factory()->post->create_and_get( [ 'post_title' => 'Hello World', ] ); $this->get( $post->permalink() ) ->assertOk() ->assertSee( 'Hello World' ); } } ``` All available assertions are listed in the [Available Assertions](#available-assertions) section. The following sections will provide examples of how to use some of the most common assertions with some examples for specific types of responses. #### Testing HTML Responses[​](#testing-html-responses "Direct link to Testing HTML Responses") HTML responses can be tested against using various methods to assert the response, including `assertSee()`, `assertElementExists()`, `assertElementMissing()`, `assertQuerySelectorExists()`, and `assertQuerySelectorMissing()`. The `assertElementExists()` and `assertElementMissing()` methods use [DOMXPath](https://www.php.net/manual/en/class.domxpath.php) to validate if a element exists on the page. The `assertQuerySelectorExists()` and `assertQuerySelectorMissing()` methods use the [Symfony CSS Selector](https://symfony.com/doc/current/components/css_selector.html) to validate if a element exists on the page. ```php $this->get( '/example' ) ->assertOk() ->assertSee( 'mantle' ) ->assertElementExists( '//script[@data-testid="example-script"]' ) ->assertQuerySelectorExists( 'script[data-testid="example-script"]' ); ``` You can also use other methods to assert if the response has/does not have an element by class or ID. ```php $this->get( '/example' ) ->assertOk() // By class name. ->assertElementExistsByClass( 'example-class' ) ->assertElementMissingByClass( 'invalid-class' ) // By element ID. ->assertElementExistsById( 'example-id' ) ->assertElementMissingById( 'invalid-id' ) // By tag name. ->assertElementExistsByTagName( 'div' ) ->assertElementMissingByTagName( 'script' ); ``` For more information see [Element Assertions](#element-assertions). ##### Query Assertions[​](#query-assertions "Direct link to Query Assertions") Mantle supports making assertions against the global WordPress query and its related queried object/ID. You may use `assertQueryTrue()` to assert the given WP\_Query `is_` functions (`is_single()`, `is_archive()`, etc.) return true and all others return false. ```php $this->get( static::factory()->post->create_and_get() ) ->assertQueryTrue( 'is_single', 'is_singular' ); $this->get( '/' ) ->assertQueryTrue( 'is_home', 'is_front_page ); ``` You may use `assertQueriedObjectId()` to assert the given ID matches the result of `get_queried_object_id()`. `assertQueriedObject()` can be used to assert that the type and ID of the given object match that of `get_queried_object()`. ```php $this->get( static::factory()->post->create_and_get() ) ->assertQueriedObjectId( $post->ID ) ->assertQueriedObject( $post ); ``` ##### Debugging Responses[​](#debugging-responses "Direct link to Debugging Responses") After making a test request, you may use the `dump` or `dump_headers` methods to dump the response body or headers to the console. ```php $this->get( '/example' ) ->dump() ->dump_headers(); ``` Alternatively, you may use the `dd` or `dd_headers` methods to print the response body or headers and end the test. ```php $this->get( '/example' )->dd(); $this->get( '/example' )->dd_headers(); ``` You can use the `dumpJson()`/`ddJson()` methods to dump the JSON response body from a (optional) specific path. ```php $this->get( '/example' ) ->dumpJson() ->dumpJson( 'data' ); $this->get( '/example' )->ddJson(); ``` #### Testing JSON APIs[​](#testing-json-apis "Direct link to Testing JSON APIs") Mantle includes several helpers for testing JSON APIs and their responses. For example, `assertJsonPath()` can be used to easily check the response back from a JSON API. This supports both custom routes and the WordPress REST API. JSON-requests can also be made via helper functions `json()`, `get_json()`, `post_json()`, and `put_json()`. The `assertJsonPath()` supports passing in a XPath to compare a specific element inside a JSON response. In the example below, we'll be retrieving the `id` element from the JSON object returned and comparing the value against an expected value. ```php $post_id = static::factory()->post->create(); $this->get( rest_url( "/wp/v2/posts/{$post_id}" ) ) ->assertJsonPath( 'id', $post_id ); ``` For more information see [JSON Assertions](#json-assertions). ##### Asserting Exact JSON Matches[​](#asserting-exact-json-matches "Direct link to Asserting Exact JSON Matches") As previously mentioned, the `assertJsonPath()` method may be used to assert that a specific JSON. There are also times when you wish to match the JSON response exactly. Using `assertJsonMissing()` Mantle will compare the response back and assert if the JSON returned matches the expected value. ```php $this->get( rest_url( '/mantle/v1/example' ) ) ->assertJsonMissing( [ 'key' => 'value', // ... ] ); ``` `assertJsonMissingExact()` can also be used to assert that the response does not contain the exact JSON fragment. ```php $this->get( rest_url( '/mantle/v1/example' ) ) ->assertOk() ->assertJsonMissingExact( [ 'invalid' => 'value', ] ); ``` #### Testing Custom Dispatch Paths[​](#testing-custom-dispatch-paths "Direct link to Testing Custom Dispatch Paths") *AKA testing pages that call `exit` or `die` directly.* Sometimes you may have a page that calls `exit` or `die` directly, for example in a custom dispatch path that renders and exists before WordPress even calls `parse_request`. Testing these pages can be difficult because PHP will exit the script when `exit` or `die` is called and PHPUnit will not be able to continue running the test. To work around this, Mantle includes a `terminate_request()` helper that can be used to either trigger an exception if we are unit testing or exit the script if we are not. Here's an example of how to use the `terminate_request()` helper in a custom dispatch path: ```php use function Mantle\Support\Helpers\terminate_request; if ( '/example-path/' === $_SERVER['REQUEST_URI'] ) { // Render the page. echo '

Example Page

'; // Terminate the request. terminate_request(); } ``` You can then test this page using the `get()` method as usual: ```php $this->get( '/example-path/' ) ->assertOk() ->assertSee( 'Example Page' ); ``` The `terminate_request()` helper will check if we are unit testing. If we are, it will simulate an `exit` by throwing a `Mantle\Testing\Exceptions\Exit_Simulation_Exception` exception. This allows PHPUnit to catch the exception and continue running the test. If we are not unit testing, it will call `exit` as normal. #### Before/After Callbacks[​](#beforeafter-callbacks "Direct link to Before/After Callbacks") You may use `before_request()` and `after_request()` to register callbacks to be run before and after each test request. These callbacks are useful for registering and unregistering hooks, filters, and other WordPress functionality that may be required for your tests. ```php namespace App\Tests; class Example_Callback_Test extends Test_Case { protected function setUp(): void { parent::setUp(); $this->before_request( function() { // Register hooks, filters, etc. that should apply to HTTP requests. } ); $this->after_request( function() { // Unregister hooks, filters, etc. } ); } public function test_example_callbacks() { // ... } } ``` #### Available Assertions[​](#available-assertions "Direct link to Available Assertions") `Mantle\Testing\Test_Response` provides many assertions to confirm aspects of the response return as expected. * [HTTP Status Assertions](#http-status-assertions) * [assertSuccessful](#assertsuccessful) * [assertOk](#assertok) * [assertStatus](#assertstatus) * [assertCreated](#assertcreated) * [assertContent](#assertcontent) * [assertNoContent](#assertnocontent) * [assertNotFound](#assertnotfound) * [assertForbidden](#assertforbidden) * [assertUnauthorized](#assertunauthorized) * [assertRedirect](#assertredirect) * [Element Assertions](#element-assertions) * [assertQuerySelectorExists](#assertqueryselectorexists) * [assertQuerySelectorMissing](#assertqueryselectormissing) * [assertElementExists](#assertelementexists) * [assertElementMissing](#assertelementmissing) * [assertElementExistsByClass](#assertelementexistsbyclass) * [assertElementMissingByClass](#assertelementmissingbyclass) * [assertElementExistsById](#assertelementexistsbyid) * [assertElementMissingById](#assertelementmissingbyid) * [assertElementExistsByTagName](#assertelementexistsbytagname) * [assertElementMissingByTagName](#assertelementmissingbytagname) * [assertElementCount](#assertelementcount) * [assertQuerySelectorCount](#assertqueryselectorcount) * [assertElementExistsByTestId](#assertelementexistsbytestid) * [assertElementMissingByTestId](#assertelementmissingbytestid) * [assertElement](#assertelement) * [assertQuerySelector](#assertqueryselector) * [Header Assertions](#header-assertions) * [assertLocation](#assertlocation) * [assertHeader](#assertheader) * [assertHeaderMissing](#assertheadermissing) * [JSON Assertions](#json-assertions) * [assertJsonPath](#assertjsonpath) * [assertJsonPathExists](#assertjsonpathexists) * [assertJsonPathMissing](#assertjsonpathmissing) * [assertExactJson](#assertexactjson) * [assertJsonFragment](#assertjsonfragment) * [assertJsonMissing](#assertjsonmissing) * [assertJsonMissingExact](#assertjsonmissingexact) * [assertJsonCount](#assertjsoncount) * [assertJsonStructure](#assertjsonstructure) * [assertIsJson](#assertisjson) * [assertIsNotJson](#assertisnotjson) * [Content Body Assertions](#content-body-assertions) * [assertSee](#assertsee) * [assertSeeInOrder](#assertseeinorder) * [assertSeeText](#assertseetext) * [assertSeeTextInOrder](#assertseetextinorder) * [assertDontSee](#assertdontsee) * [assertDontSeeText](#assertdontseetext) * [WordPress Query Assertions](#wordpress-query-assertions) * [assertQueryTrue](#assertquerytrue) * [assertQueriedObjectId](#assertqueriedobjectid) * [assertQueriedObject](#assertqueriedobject) ##### HTTP Status Assertions[​](#http-status-assertions "Direct link to HTTP Status Assertions") ###### assertSuccessful[​](#assertsuccessful "Direct link to assertSuccessful") Assert that a response has a (>= 200 and < 300) HTTP status code. ```php $response->assertSuccessful(); ``` ###### assertOk[​](#assertok "Direct link to assertOk") Assert that a response has a 200 HTTP status code. ```php $response->assertOk(); ``` ###### assertStatus[​](#assertstatus "Direct link to assertStatus") Assert that a response has a given HTTP status code. ```php $response->assertStatus( $status ); ``` ###### assertCreated[​](#assertcreated "Direct link to assertCreated") Assert that a response has a 201 HTTP status code. ```php $response->assertCreated(); ``` ###### assertContent[​](#assertcontent "Direct link to assertContent") Assert that a response has a given status code (default to 200) and content. ```php $response->assertContent( mixed $value ); ``` Also supports passing a callable to assert the content, which will be executed with the response body as the first argument. This is useful for making custom assertions against the response body. The callable should return `true` if the assertion passes, or `false` if it fails. ```php $response->assertContent( function( string $body ) { return str_contains( $body, 'Hello World' ); } ); ``` ###### assertNoContent[​](#assertnocontent "Direct link to assertNoContent") Assert that a response has a given status code (default to 204) and no content. ```php $response->assertNoContent( int $status = 204 ); ``` ###### assertNotFound[​](#assertnotfound "Direct link to assertNotFound") Assert that a response has a 404 HTTP status code. ```php $response->assertNotFound(); ``` ###### assertForbidden[​](#assertforbidden "Direct link to assertForbidden") Assert that a response has a 403 HTTP status code. ```php $response->assertForbidden(); ``` ###### assertUnauthorized[​](#assertunauthorized "Direct link to assertUnauthorized") Assert that a response has a 401 HTTP status code. ```php $response->assertUnauthorized(); ``` ###### assertRedirect[​](#assertredirect "Direct link to assertRedirect") Assert that a response has a redirect to a given URI and has a 301 or 302 HTTP status code. ```php $response->assertRedirect( $uri = null ); ``` ##### Element Assertions[​](#element-assertions "Direct link to Element Assertions") Element assertions are used to assert the presence, absence, etc. of elements in the response body. These assertions use [DOMXPath](https://www.php.net/manual/en/class.domxpath.php) and support query selectors via the `symfony/css-selector` package. Are you looking to assert against a HTML string that is not a response? If you are looking to assert against a HTML string that is not a response, you can use the [HTML String](/docs/testing/helpers.md#html-string) helper to make assertions. ###### assertQuerySelectorExists[​](#assertqueryselectorexists "Direct link to assertQuerySelectorExists") Assert that a given CSS selector exists in the response. ```php $response->assertQuerySelectorExists( string $selector ); ``` ###### assertQuerySelectorMissing[​](#assertqueryselectormissing "Direct link to assertQuerySelectorMissing") Assert that a given CSS selector does not exist in the response. ```php $response->assertQuerySelectorMissing( string $selector ); ``` ###### assertElementExists[​](#assertelementexists "Direct link to assertElementExists") Assert that a given XPath exists in the response. ```php $response->assertElementExists( string $expression ); ``` ###### assertElementMissing[​](#assertelementmissing "Direct link to assertElementMissing") Assert that a given XPath does not exist in the response. ```php $response->assertElementMissing( string $expression ); ``` ###### assertElementExistsByClass[​](#assertelementexistsbyclass "Direct link to assertElementExistsByClass") Assert that a given class exists in the response. ```php $response->assertElementExistsByClass( string $class ); ``` ###### assertElementMissingByClass[​](#assertelementmissingbyclass "Direct link to assertElementMissingByClass") Assert that a given class does not exist in the response. ```php $response->assertElementMissingByClass( string $class ); ``` ###### assertElementExistsById[​](#assertelementexistsbyid "Direct link to assertElementExistsById") Assert that a given ID exists in the response. ```php $response->assertElementExistsById( string $id ); ``` ###### assertElementMissingById[​](#assertelementmissingbyid "Direct link to assertElementMissingById") Assert that a given ID does not exist in the response. ```php $response->assertElementMissingById( string $id ); ``` ###### assertElementExistsByTagName[​](#assertelementexistsbytagname "Direct link to assertElementExistsByTagName") Assert that a given tag name exists in the response. ```php $response->assertElementExistsByTagName( string $tag_name ); ``` ###### assertElementMissingByTagName[​](#assertelementmissingbytagname "Direct link to assertElementMissingByTagName") Assert that a given tag name does not exist in the response. ```php $response->assertElementMissingByTagName( string $tag_name ); ``` ###### assertElementCount[​](#assertelementcount "Direct link to assertElementCount") Assert that the response has the expected number of elements matching the given XPath expression. ```php $response->assertElementCount( string $expression, int $expected ); ``` ###### assertQuerySelectorCount[​](#assertqueryselectorcount "Direct link to assertQuerySelectorCount") Assert that the response has the expected number of elements matching the given CSS selector. ```php $response->assertQuerySelectorCount( string $selector, int $expected ); ``` ###### assertElementExistsByTestId[​](#assertelementexistsbytestid "Direct link to assertElementExistsByTestId") Assert that an element with the given `data-testid` attribute exists in the response. ```php $response->assertElementExistsByTestId( string $test_id ); ``` ###### assertElementMissingByTestId[​](#assertelementmissingbytestid "Direct link to assertElementMissingByTestId") Assert that an element with the given `data-testid` attribute does not exist in the response. ```php $response->assertElementMissingByTestId( string $test_id ); ``` ###### assertElement[​](#assertelement "Direct link to assertElement") Assert that the given element exists in the response and passes the given assertion. This can be used to make custom assertions against the element that cannot be expressed in a simple XPath expression or query selector. ```php $response->assertElement( string $expression, callable $assertion, bool $pass_any = false ); ``` If `$pass_any` is `true`, the assertion will pass if any of the elements pass the assertion. Otherwise, all elements must pass the assertion. Let's take a look at an example: ```php use DOMElement; $response->assertElement( '//div', fn ( DOMElement $element ) => $this->assertEquals( 'Hello World', $element->textContent ) && $this->assertNotEmpty( $element->getAttribute( 'class' ) ) ); }, ); ``` ###### assertQuerySelector[​](#assertqueryselector "Direct link to assertQuerySelector") Assert that the given CSS selector exists in the response and passes the given assertion. Similar to `assertElement`, this can be used to make custom assertions against the element that cannot be expressed in a simple XPath expression or query selector. ```php $response->assertQuerySelector( string $selector, callable $assertion, bool $pass_any = false ); ``` Let's take a look at an example: ```php use DOMElement; $response->assertQuerySelector( 'div > p', fn ( DOMElement $element ) => $this->assertEquals( 'Hello World', $element->textContent ) && $this->assertNotEmpty( $element->getAttribute( 'class' ) ) ); }, ); ``` ##### Header Assertions[​](#header-assertions "Direct link to Header Assertions") ###### assertLocation[​](#assertlocation "Direct link to assertLocation") Assert that the response has a `Location` header matching the given URI. ```php $response->assertLocation( $uri ); ``` ###### assertHeader[​](#assertheader "Direct link to assertHeader") Assert that the response has a given header and optionally the given value. ```php $response->assertHeader( $header_name, $value = null ); ``` ###### assertHeaderMissing[​](#assertheadermissing "Direct link to assertHeaderMissing") Assert that the response does not have a given header and optional value. ```php $response->assertHeaderMissing( $header_name ); $response->assertHeaderMissing( $header_name, $value ); ``` ##### JSON Assertions[​](#json-assertions "Direct link to JSON Assertions") ###### assertJsonPath[​](#assertjsonpath "Direct link to assertJsonPath") Assert that the expected value and type exists at the given path in the response. ```php $response->assertJsonPath( string $path, $expect ); ``` ###### assertJsonPathExists[​](#assertjsonpathexists "Direct link to assertJsonPathExists") Assert that a specific JSON path exists. ```php $response->assertJsonPathExists( string $path ); ``` ###### assertJsonPathMissing[​](#assertjsonpathmissing "Direct link to assertJsonPathMissing") Assert that a specific JSON path is missing. ```php $response->assertJsonPathMissing( string $path ); ``` ###### assertExactJson[​](#assertexactjson "Direct link to assertExactJson") Assert that the response has the exact given JSON. ```php $response->assertExactJson( array $data ); ``` ###### assertJsonFragment[​](#assertjsonfragment "Direct link to assertJsonFragment") Assert that the response contains the given JSON fragment. ```php $response->assertJsonFragment( array $data); ``` ###### assertJsonMissing[​](#assertjsonmissing "Direct link to assertJsonMissing") Assert that the response does not contain the given JSON fragment. ```php $response->assertJsonMissing( array $data ); ``` ###### assertJsonMissingExact[​](#assertjsonmissingexact "Direct link to assertJsonMissingExact") Assert that the response does not contain the exact JSON fragment. ```php $response->assertJsonMissingExact( array $data ); ``` ###### assertJsonCount[​](#assertjsoncount "Direct link to assertJsonCount") Assert that the response JSON has the expected count of items at the given key. ```php $response->assertJsonCount( int $count, string $key = null ); ``` ###### assertJsonStructure[​](#assertjsonstructure "Direct link to assertJsonStructure") Assert that the response has a given JSON structure. ```php $response->assertJsonStructure( array $structure = null); ``` ###### assertIsJson[​](#assertisjson "Direct link to assertIsJson") Assert that the response has a valid JSON structure and `content-type` header with `application/json`. ```php $response->assertIsJson(); ``` ###### assertIsNotJson[​](#assertisnotjson "Direct link to assertIsNotJson") Assert that the response does not have a valid JSON structure or `content-type` header with `application/json`. ```php $response->assertIsNotJson(); ``` ##### Content Body Assertions[​](#content-body-assertions "Direct link to Content Body Assertions") ###### assertSee[​](#assertsee "Direct link to assertSee") Assert the given string exists in the body content ```php $response->assertSee( $value ); ``` ###### assertSeeInOrder[​](#assertseeinorder "Direct link to assertSeeInOrder") Assert the given strings exist in the body content in the given order ```php $response->assertSeeInOrder( array $values ); ``` ###### assertSeeText[​](#assertseetext "Direct link to assertSeeText") Similar to `assertSee()` but strips all HTML tags first ```php $response->assertSeeText( $value ); ``` ###### assertSeeTextInOrder[​](#assertseetextinorder "Direct link to assertSeeTextInOrder") Similar to `assertSeeInOrder()` but strips all HTML tags first ```php $response->assertSeeTextInOrder( array $values ); ``` ###### assertDontSee[​](#assertdontsee "Direct link to assertDontSee") Assert the given string does not exist in the body content. ```php $response->assertDontSee( $value ); ``` ###### assertDontSeeText[​](#assertdontseetext "Direct link to assertDontSeeText") Similar to `assertDontSee()` but strips all HTML tags first. ```php $response->assertDontSeeText( $value ); ``` ##### WordPress Query Assertions[​](#wordpress-query-assertions "Direct link to WordPress Query Assertions") ###### assertQueryTrue[​](#assertquerytrue "Direct link to assertQueryTrue") Assert the given `WP_Query`'s `is_` functions (`is_single()`, `is_archive()`, etc.) return true and all others return false ```php $response->assertQueryTrue( ...$prop ); ``` ###### assertQueriedObjectId[​](#assertqueriedobjectid "Direct link to assertQueriedObjectId") Assert the given ID matches the result of `get_queried_object_id()`. ```php $response->assertQueriedObjectId( int $id ); ``` ###### assertQueriedObject[​](#assertqueriedobject "Direct link to assertQueriedObject") Assert that the type and ID of the given object match that of `get_queried_object()`. ```php $response->assertQueriedObject( $object ); ``` --- ### Testing: Snapshot Testing Mantle's testing framework includes [Spatie's PHPUnit Snapshot Testing package](https://github.com/spatie/phpunit-snapshot-assertions) for easy snapshot testing in your unit tests. It is a wonderful way to test without writing actual test cases, comparing the outcome of functions/methods to a snapshot of the expected output. ```php use Mantle\Testkit\TestCase; class Example_Test extends Test_Case { public function test_it_can_do_something() { // Perform some action that should result in a specific output. $output = do_something(); // Assert that the output matches the snapshot. $this->assertMatchesSnapshot( $output ); } } ``` After running the test, you will see a new snapshot file in the `__snapshots__` relative to the current test that will store the output of the test. If the output of the test changes, the test will fail and you will be able to review the changes and decide whether to accept them or not. If you need to update the stored snapshot, you can either delete the `__snapshots__` directory relative to your test or you can pass `-d --update-snapshots` to the test runner. For more information about snapshot testing, see the [Spatie's PHPUnit Snapshot Package Documentation](https://github.com/spatie/phpunit-snapshot-assertions). #### Testing Requests[​](#testing-requests "Direct link to Testing Requests") [HTTP request tests](/docs/testing/requests.md) can use snapshot testing to test the response of a remote request. This is useful for testing the response of a an REST API endpoint OR a page on the site. Snapshot testing can help you test against the entire response of a page rather than checking for specific parts of it. For example, if you had a login page, you could test the entire page response to ensure that the login form is present, the page title is correct, and the page is not returning an error. ```php use Mantle\Testkit\TestCase; class Example_Test extends Test_Case { public function test_it_can_do_something() { $this->get( '/login' )->assertMatchesSnapshot(); } } ``` ##### `assertMatchesSnapshot()` / `assertMatchesSnapshotContent()`[​](#assertmatchessnapshot--assertmatchessnapshotcontent "Direct link to assertmatchessnapshot--assertmatchessnapshotcontent") Assert that the given content matches the snapshot. Only compares the content to a stored snapshot. **Note:** If you are testing against a JSON response, the method will automatically use `assertMatchesSnapshotJson()` to compare the response. ```php $this->get( '/' )->assertMatchesSnapshot(); ``` ##### `assertMatchesSnapshotHtml()`[​](#assertmatchessnapshothtml "Direct link to assertmatchessnapshothtml") Assert that a response matches the snapshot. Expects the content to be valid HTML. Internally the package will be loaded into a DOMDocument and the HTML will be formatted before it is compared to the snapshot. See [the HTML Driver](https://github.com/spatie/phpunit-snapshot-assertions/blob/main/src/Drivers/HtmlDriver.php) for more information. ```php $this->get( '/' )->assertMatchesSnapshotHtml(); ``` ##### `assertMatchesSnapshotJson()`[​](#assertmatchessnapshotjson "Direct link to assertmatchessnapshotjson") Assert that a response matches the snapshot. Expects the content to be valid JSON. Internally the package will be JSON decoded and encoded again for storage. ```php $this->get( '/wp-json/wp/v2/posts' )->assertMatchesSnapshotJson(); ``` `assertMatchesSnapshotJson()` also accepts a second parameter to specify the JSON paths that can be included in the snapshot. This is useful if you want to test the response of an API endpoint but don't want to test the entire response. This supports `*` wildcards. ```php $this->get( '/wp-json/wp/v2/posts' )->assertMatchesSnapshotJson( '*.type', ); $this->get( '/wp-json/wp/v2/posts' )->assertMatchesSnapshotJson( [ 'data.*.type', 'data.*.title', 'data.*.content', ] ); $this->get( '/wp-json/wp/v2/posts' )->assertMatchesSnapshotJson( [ 'single-key', ] ); ``` ##### `assertStatusAndHeadersMatchSnapshot()`[​](#assertstatusandheadersmatchsnapshot "Direct link to assertstatusandheadersmatchsnapshot") Assert that the status code and headers match the snapshot. This is useful if you want to test the response of an endpoint but don't want to test the response's content. Use with caution! This is useful for testing the response of an endpoint but it is not recommended for practice testing because headers often contain dynamic information such as the date and time. If you want to test the response of an endpoint, it is recommended to use `assertMatchesSnapshot()` method. ```php $this->get( '/wp-json/wp/v2/posts' )->assertStatusAndHeadersMatchSnapshot(); ``` ##### `assertMatchesSnapshotWithStatusAndHeaders()`[​](#assertmatchessnapshotwithstatusandheaders "Direct link to assertmatchessnapshotwithstatusandheaders") Assert that the status code, headers, and content match the snapshot. This is useful if you want to test the entire response of the endpoint (status code, headers, and content). ```php $this->get( '/wp-json/wp/v2/posts' )->assertMatchesSnapshotWithStatusAndHeaders(); ``` --- ### Testing: WordPress State #### Introduction[​](#introduction "Direct link to Introduction") During unit tests, the testing framework exposes some helper methods to allow you to modify or inspect the state of WordPress. These methods are available on the default `Test_Case` included with the framework or Testkit. Many of these methods are used between tests to reset the state of WordPress. #### Preserving Globals[​](#preserving-globals "Direct link to Preserving Globals") By default, the testing framework will preserve select global variables between tests. This means that if a test modifies a global variable that manages the registered post types, the modified state will be reset before the next test runs. Let's look at an example: ```php namespace App\Tests; use App\Tests\TestCase; class ExampleTest extends TestCase { public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); register_post_type( 'custom', [ 'label' => 'Custom', ] ); } public function test_post_type_exists() { // The post type 'custom' was registered in setUpBeforeClass. We know it exists for this test case. $this->assertTrue( post_type_exists( 'custom' ) ); } } ``` In another test case, the `custom` post type will not exist because the framework resets the registered post types between tests. ```php namespace App\Tests; use App\Tests\TestCase; class AnotherExampleTest extends TestCase { public function test_post_type_does_not_exist() { // The post type 'custom' does not exist in this test case. $this->assertFalse( post_type_exists( 'custom' ) ); } } ``` The framework will create a copy of the globals before all tests run and another after the test's `setUpBeforeClass` method is called. For each test run, the local copy is restored before the test's `setUp` method is called. After all tests have run, the original copy is restored. The following globals are preserved between tests: * `wp_meta_keys`: Custom meta keys registered via `register_meta()`. * `wp_post_statuses`: Post statuses registered via `register_post_status()`. * `wp_post_types`: Post types registered via `register_post_type()`. * `wp_taxonomies`: Taxonomies registered via `register_taxonomy()`. If you'd like to disable the global preservation for a specific test case, you can use the `Mantle\Testing\Attributes\DisableGlobalPreservation` attribute on your test class or method. #### Updating the Post Modified Time[​](#updating-the-post-modified-time "Direct link to Updating the Post Modified Time") The `update_post_modified` method can be used to update the `post_modified` time for a post. This method accepts the post ID and the new timestamp to use. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_update_post_modified() { $post_id = $this->factory()->post->create(); $this->update_post_modified( $post_id, '2019-01-01 00:00:00' ); $post = get_post( $post_id ); $this->assertEquals( '2019-01-01 00:00:00', $post->post_modified ); } } ``` #### Reset Post Statuses[​](#reset-post-statuses "Direct link to Reset Post Statuses") The `reset_post_statuses` method can be used to reset the post statuses to their default values. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_reset_post_statuses() { register_post_status( 'custom', [ 'label' => 'Custom', ] ); $this->reset_post_statuses(); $this->assertFalse( get_post_status_object( 'custom' ) ); } } ``` #### Reset Post Types[​](#reset-post-types "Direct link to Reset Post Types") The `reset_post_types` method can be used to reset the post types to their default values. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_reset_post_types() { register_post_type( 'custom', [ 'label' => 'Custom', ] ); $this->reset_post_types(); $this->assertFalse( get_post_type_object( 'custom' ) ); } } ``` #### Reset Taxonomies[​](#reset-taxonomies "Direct link to Reset Taxonomies") The `reset_taxonomies` method can be used to reset the taxonomies to their default values. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_reset_taxonomies() { register_taxonomy( 'custom', 'post', [ 'label' => 'Custom', ] ); $this->reset_taxonomies(); $this->assertFalse( get_taxonomy( 'custom' ) ); } } ``` #### Flushing Cache[​](#flushing-cache "Direct link to Flushing Cache") The `flush_cache` method can be used to flush the WordPress object cache. This method is called automatically between tests. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_flush_cache() { wp_cache_set( 'foo', 'bar' ); $this->flush_cache(); $this->assertFalse( wp_cache_get( 'foo' ) ); } } ``` #### Deleting User[​](#deleting-user "Direct link to Deleting User") The `delete_user` method can be used to delete a user. This method accepts the user ID to delete. ```php namespace App\Tests; class Example_Test extends TestCase { public function test_delete_user() { $user_id = $this->factory()->user->create(); $this->delete_user( $user_id ); $this->assertFalse( get_user_by( 'id', $user_id ) ); } } ``` #### Setting Front Page / Blog Page[​](#setting-front-page--blog-page "Direct link to Setting Front Page / Blog Page") The `set_show_posts_on_front()` and `set_show_page_on_front()` methods can be used to set the front page display settings. This mirrors what is controlled in WordPress via `Settings > Reading`. ```php namespace App\Tests; class Example_Test extends Test_Case { public function test_front_page_shows_posts() { $this->set_show_posts_on_front(); $this->assertEquals( 'posts', get_option( 'show_on_front' ) ); // Create a post. $post = static::factory()->post->create_and_get(); // Verify that the front page shows the latest posts. $this->get( '/' )->assertSee( $post->post_title ); } public function test_front_page_shows_a_specific_page() { $page = $this->factory()->page->create_and_get(); $this->set_show_page_on_front( front: $page ); // Verify that the front page shows the specific page. $this->get( '/' )->assertQueriedObject( $page ); } public function test_front_page_shows_a_specific_page_with_separate_posts_page(): void { $page = $this->factory()->page->create_and_get(); $posts_page = $this->factory()->page->create_and_get(); $this->set_show_page_on_front( front: $page, posts: $posts_page ); // Verify that the front page shows the specific page. $this->get( '/' ) ->assertQueriedObject( $page ) ->assertQueryTrue( 'is_front_page', 'is_page', 'is_singular' ); // Verify that the posts page shows the latest posts. $this->get( get_permalink( $posts_page ) ) ->assertQueriedObject( $posts_page ) ->assertQueryTrue( 'is_home' ); // is_home is true for the posts page. } } ``` --- ### Testing: Mantle Testkit Mantle Testkit is a standalone package for using the [Mantle Testing Framework](/docs/testing.md) on non-Mantle based projects. That means that you can use the great features of the Mantle Testing Framework on your existing projects/plugins/themes without needing to do any more refactoring. This document acts as a guide to transitioning your project to using [Mantle Testkit](/docs/testing/testkit.md) for use of the Mantle Testing Library outside of Mantle. info Testkit should only be used on projects that do not use or define a Mantle application. #### Getting Started[​](#getting-started "Direct link to Getting Started") This guide assumes that you are working within a `wp-content/`-rooted WordPress project. ##### Install `mantle-framework/testkit` as a dependency[​](#install-mantle-frameworktestkit-as-a-dependency "Direct link to install-mantle-frameworktestkit-as-a-dependency") ```bash composer require --dev mantle-framework/testkit ``` ##### Change Test Case[​](#change-test-case "Direct link to Change Test Case") Unit Tests should extend themselves from Testkit's `Mantle\Testkit\TestCase` class in place of core's `WP_UnitTestCase` class. ```diff use Mantle\Testkit\TestCase; -abstract class ExampleTest extends WP_UnitTestCase { +abstract class ExampleTest extends TestCase { public function test_example() { $this->go_to( home_url( '/' ) ); $this->assertQueryTrue( 'is_home', 'is_archive' ); } public function test_factory() { $post = static::factory()->post->create_and_get(); // WP_Post. // ... } } ``` ##### Adjusting Unit Test Bootstrap[​](#adjusting-unit-test-bootstrap "Direct link to Adjusting Unit Test Bootstrap") Commonly unit tests live inside of plugins or themes. For this use case, we're going to adjust a theme's unit test bootstrap file to load the test framework. Mantle will already be loaded from PHPUnit. tests/bootstrap.php ```php /** * Testing using Mantle Framework */ // Install Mantle Testing Framework normally with no modifications. \Mantle\Testing\install(); ``` If you need to customize the installation of the Testing Framework (e.g., setting the theme, adding plugins, etc.), you can use the Installation Manager: tests/bootstrap.php ```php /** * Testing using Mantle Framework */ \Mantle\Testing\manager() ->before( ... ) ->after( ... ) ->theme( 'twentytwenty' ) ->loaded( function() { // The loaded callback is fired on 'muplugins_loaded'. // You can use this callback to load the main file of a plugin, theme, etc. // Setup any dependencies once WordPress is loaded... } ); ``` For more information, read more about the [Installation Manager](/docs/testing/installation-manager.md). ##### Running Tests[​](#running-tests "Direct link to Running Tests") Run your tests using `./vendor/bin/phpunit` or add a Composer script to allow for `composer phpunit`. You do not need any additional bash script to run the tests. --- ### Testing: Traits and Attributes #### Introduction[​](#introduction "Direct link to Introduction") Mantle's Test Framework uses traits and attributes to add optional functionality to a test case. #### Traits[​](#traits "Direct link to Traits") ##### Refresh Database[​](#refresh-database "Direct link to Refresh Database") The `Mantle\Testing\Concerns\Refresh_Database` trait will ensure that the database is rolled back after each test in the test case has run. Without it, data in the database will persist between tests, which almost certainly would not be desirable. That said, if your test case doesn't interact with the database, omitting this trait will provide a significant performance boost. ##### Admin Screen[​](#admin-screen "Direct link to Admin Screen") The `Mantle\Testing\Concerns\Admin_Screen` and `Mantle\Testing\Concerns\Network_Admin_Screen` traits will set the current "screen" to a WordPress admin screen, and `is_admin()` will return true in tests in the test case. This is useful for testing admin screens, or any code that checks `is_admin()`. ##### Network Admin Screen[​](#network-admin-screen "Direct link to Network Admin Screen") The `Mantle\Testing\Concerns\Network_Admin_Screen` trait will set the current "screen" to a WordPress network admin screen, and `is_network_admin()` will return true in tests in the test case. This is useful for testing network admin screens, or any code that checks `is_network_admin()`. ##### With Faker[​](#with-faker "Direct link to With Faker") The `Mantle\Testing\Concerns\With_Faker` trait will add a `$this->faker` property to the test case with an instance of [Faker](https://fakerphp.github.io/). This is useful for generating test data. ##### Prevent Remote Requests[​](#prevent-remote-requests "Direct link to Prevent Remote Requests") The `Mantle\Testing\Concerns\Prevent_Remote_Requests` trait will prevent remote requests from being made during tests. This is useful for testing code that makes remote requests, but you don't want to actually make the requests or fake them. ##### Warn Remote Requests[​](#warn-remote-requests "Direct link to Warn Remote Requests") The `Mantle\Testing\Concerns\Warn_Remote_Requests` trait will warn when a test makes a remote request without providing a default response. ##### Reset Data Structures[​](#reset-data-structures "Direct link to Reset Data Structures") The `Mantle\Testing\Concerns\Reset_Data_Structures` trait will reset data structures between tests. This will reset all post types and taxonomies that are registered before each test is run. ##### Reset Server[​](#reset-server "Direct link to Reset Server") The `Mantle\Testing\Concerns\Reset_Server` trait will reset the server state between tests. This will clear the main `$_SERVER` superglobals before each test run, including `$_SERVER['REQUEST_URI']`, `$_SERVER['REQUEST_METHOD']`, and `$_SERVER['HTTP_HOST']`. ##### Multisite / Single Site Test[​](#multisite--single-site-test "Direct link to Multisite / Single Site Test") The `Multisite_Test` / `Single_Site_Test` traits can be used to skip a test if the current site is not a multisite or single site, respectively. Multisite test example: ```php use App\Tests\TestCase; use Mantle\Testing\Concerns\Multisite_Test; class ExampleTest extends TestCase { use Multisite_Test; public function test_example(): void { // This test will be skipped if the current test is not running multisite. } } ``` Single site test example: ```php use App\Tests\TestCase; use Mantle\Testing\Concerns\Single_Site_Test; class ExampleTest extends TestCase { use Single_Site_Test; public function test_example(): void { // This test will be skipped if the current test is running multisite. } } ``` #### Attributes[​](#attributes "Direct link to Attributes") The following are [PHP Attributes](https://www.php.net/manual/en/language.attributes.overview.php) that can be used to add optional functionality to a test case. Many can be used on test classes or individual test methods. Check the documentation for each attribute for details on how to use it. ##### Preserve Object Cache[​](#preserve-object-cache "Direct link to Preserve Object Cache") When making HTTP requests in tests, the object cache will be cleared before each request. If you want to preserve the object cache between requests, you can use the `Mantle\Testing\Attributes\PreserveObjectCache` attribute on your test class or method. Supports test classes and individual test methods. ```php use App\Tests\TestCase; use Mantle\Testing\Attributes\PreserveObjectCache; class ExampleTest extends TestCase { #[PreserveObjectCache] public function test_example(): void { $this->get('/some-endpoint') ->assertOk(); // ... } } ``` ##### User Agent[​](#user-agent "Direct link to User Agent") You can set the user agent used for HTTP requests in tests by using the `Mantle\Testing\Attributes\UserAgent` attribute on your test class or method. The attribute includes some common user agents for desktop, tablet, and mobile devices, but you can also specify a custom user agent string. Supports test classes and individual test methods. ```php use App\Tests\TestCase; use Mantle\Testing\Attributes\UserAgent; class ExampleTest extends TestCase { #[UserAgent('My Custom User Agent')] public function test_example(): void { $this->get('/some-endpoint') ->assertOk(); // ... } #[UserAgent(UserAgent::MOBILE)] public function test_mobile_example(): void { $this->get('/some-endpoint') ->assertOk(); } } ``` ##### WordPress Environment[​](#wordpress-environment "Direct link to WordPress Environment") The WordPress `wp_get_environment_type()` function can be set when testing to a specific environment using the `Mantle\Testing\Attributes\Environment` attribute. The environment type must be one of the following: `production`, `staging`, `development`, or `local`. Supports test classes and individual test methods. ```php use App\Tests\TestCase; use Mantle\Testing\Attributes\Environment; class ExampleTest extends TestCase { #[Environment('staging')] public function test_example(): void { $this->get('/some-endpoint') ->assertOk(); // ... } #[Environment(Environment::DEVELOPMENT)] public function test_development_example(): void { $this->get('/some-endpoint') ->assertOk(); } } ``` ##### Permalink Structure[​](#permalink-structure "Direct link to Permalink Structure") The WordPress permalink structure can be quickly changed using the `Mantle\Testing\Attributes\PermalinkStructure` attribute. This attribute accepts a string that represents the permalink structure to use. The attribute will automatically flush the rewrite rules after setting the structure. By default the permalink structure of `/%year%/%monthnum%/%day%/%postname%/` is used. ```php namespace App\Tests; use Mantle\Testing\Attributes\PermalinkStructure; class Example_Test extends Test_Case { #[PermalinkStructure( '/%postname%/' )] public function test_permalink_structure() { // ... } } ``` ##### Adding Your Own Attributes[​](#adding-your-own-attributes "Direct link to Adding Your Own Attributes") You can create side effects for your own attributes using the `register_attribute()` method of the test case. Internally, this is how Mantle registers attributes such as `Acting_As` on test classes and methods. ```php namespace App\Tests\Concerns; use Mantle\Testing\Concerns\Interacts_With_Attributes; trait My_Example_Trait { use Interacts_With_Attributes; /** * Backed up global user ID. */ protected int $backup_user; /** * Backup the current global user. */ public function my_example_trait_set_up(): void { $this->backup_user = get_current_user_id(); $this->register_attribute( Acting_As::class, fn ( ReflectionAttribute $attribute ) => $this->acting_as( $attribute->newInstance()->user ), ); } } ``` --- ### Testing: Users and Authentication The Mantle Test Framework provides a method `$this->acting_as( $user )` to execute a test as a given user or a user in the given role. Passing a role name to `acting_as()` will create a new user with that role and authenticate as that user. ```php $this->acting_as( 'administrator' ); $this->assertTrue( current_user_can( 'manage_options' ) ); ``` ```php $this->acting_as( 'contributor' ); $this->get( '/some-admin-only-page/' ) ->assertForbidden(); ``` You may also pass a user instance to `acting_as()` to authenticate as that user. ```php $this->acting_as( static::factory()->user->create() ); ``` #### Using `Acting_As` Attribute[​](#using-acting_as-attribute "Direct link to using-acting_as-attribute") Test classes and methods can use the `Mantle\Testing\Attributes\Acting_As` attribute to automatically authenticate as a given user or role for a single test method or an entire test class. For example, the following test will authenticate as an administrator for the entire test class: ```php namespace Tests\Feature; use Mantle\Testing\Attributes\Acting_As; use Tests\Test_Case; #[Acting_As( 'administrator' )] class Admin_Test extends Test_Case { public function test_admin_can_manage_options() { $this->assertTrue( current_user_can( 'manage_options' ) ); } } ``` The following test will authenticate as a contributor for a single test method: ```php namespace Tests\Feature; use Mantle\Testing\Attributes\Acting_As; use Tests\Test_Case; class ContributorTest extends Test_Case { #[Acting_As( 'contributor' )] public function test_contributor_has_read_cap() { $this->assertTrue( current_user_can( 'read' ) ); } public function test_guest_has_no_caps() { $this->assertFalse( current_user_can( 'read' ) ); } } ``` #### Assertions[​](#assertions "Direct link to Assertions") The Mantle Test Framework provides assertions to make it possible to assert if you are authenticated as a given user or role or not at all. ##### `assertAuthenticated()`[​](#assertauthenticated "Direct link to assertauthenticated") Assert that the user is authenticated. ```php $this->assertAuthenticated(); ``` Also supports checking if the current user is a specific user or role: ```php $this->assertAuthenticated( 'administrator' ); $this->assertAuthenticated( $user ); ``` ##### `assertGuest()`[​](#assertguest "Direct link to assertguest") Assert that the user is not authenticated. ```php $this->assertGuest(); ``` ---