Use the MVC pattern
With models and views, the missing piece is the controller. Simply, because of its philosophy, does not impose it: the simply-mvc extension brings Symfony-like controllers to WordPress.
Install the extension
composer require clementdecou/simply-mvc
It is installed in web/content/mu-plugins/simply-mvc. Load it in config/extensions.php:
<?php
return [
'/simply-maker/entrypoint.php',
'/simply-mvc/entrypoint.php',
];
Without the boilerplate, require its entry point in your mu-plugin loader, after the framework:
<?php
/*
* Plugin Name: Load Simply framework
*/
require __DIR__ . '/simply-framework/simply-framework.php';
require __DIR__ . '/simply-mvc/entrypoint.php';
Routes follow the template hierarchy
simply-mvc does not add URLs: a route answers a request that WordPress already resolved, at the moment it would load a template. A route condition is a name of the WordPress template hierarchy: single-project replaces the single-project.php template of a theme.
Example: the page of a project
Create a controller in the src/Controller/ directory of a registered plugin:
<?php
namespace MyPlugin\Controller;
use Simply\Core\Query\SimplyQuery;
use Simply\Mvc\Attribute\Route;
use Simply\Mvc\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class ProjectController extends AbstractController
{
#[Route('single-project')]
public function single(): Response
{
return $this->render('single/project.html.twig', [
'project' => SimplyQuery::getCurrentObject(),
]);
}
}
A controller action must return a Response: an action that returns nothing fails with an explicit LogicException. render() returns a Response with the rendered template; renderView() returns only the HTML string.
Any Response works, for example new JsonResponse(['project' => $id]).
Extending AbstractController registers the controller automatically. Without it, tag the service with simply.controller and controller.service_arguments.
Generate a controller with php bin/console simply:make:controller.
Route attribute
Route(string $wpCondition, array $methods = ['GET', 'POST'], string|false $customCondition = false)
wpCondition: a WordPress condition (404,archive,attachment,author,category,front,home,page,search,single,singular,tag,tax) or any name of the template hierarchy (single-project,taxonomy-project_tag...).frontstands forfront-page, andtaxfortaxonomy.methods: the accepted HTTP methods. AHEADrequest is answered by theGETroutes.customCondition: the name of a callable that receives theRequestand returns a boolean, to match only some requests.
When several routes match, a route whose custom condition matches wins; otherwise, the most specific template of the hierarchy wins.
Action arguments
Like Symfony argument resolvers, the arguments of an action are resolved automatically:
- the request:
Symfony\Component\HttpFoundation\Request, orSimply\Mvc\Requestwhich also givesgetSimplyQuery(); - services, by their class;
- the main query: a parameter typed
WP_Query, or named$wp_query; - request attributes, the session, default values and variadic arguments.
<?php
namespace MyPlugin\Controller;
use Simply\Mvc\Attribute\Route;
use Simply\Mvc\Controller\AbstractController;
use Simply\Mvc\Request;
use Symfony\Component\HttpFoundation\Response;
class ProjectArchiveController extends AbstractController
{
#[Route('archive-project')]
public function archive(\WP_Query $wp_query, Request $request): Response
{
return $this->render('archive/project.html.twig', ['projects' => $wp_query->posts]);
}
}