undefinedfr / undefined-wp-framework
Mini Framework for Wordpress
Installs: 430
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:wordpress-boilerplate
pkg:composer/undefinedfr/undefined-wp-framework
Requires
- timber/timber: ^2.0
- vinkla/extended-acf: ^14.2
- dev-master
- 2.3
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.7.x-dev
- 2.1.7
- 2.1.6
- 2.1.5
- 2.1.3
- 2.1.2
- 2.1.0
- 2.0.6
- 2.0.4
- 2.0.2
- 2.0.1
- 2.0.0
- 1.4.4
- 1.4.1
- 1.4.0
- 1.3.6
- 1.3.5
- 1.3.4
- 1.3.3
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.12
- 1.2.11
- 1.2.10
- 1.2.8
- 1.2.7
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.2
- 1.2.1
- 1.2.0
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- dev-release/2.2.3
- dev-release/2.2.2
- dev-release/2.2.1
- dev-release/2.2.0
- dev-release/2.1.7
- dev-release/2.1.6
- dev-release/2.1.0
- dev-release/2.0.0
- dev-release/2.0
This package is auto-updated.
Last update: 2026-02-21 21:22:04 UTC
README
Mini Framework MVC pour WordPress basé sur Timber/Twig.
Installation
composer require undefinedfr/undefined-wp-framework
Dans votre functions.php :
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/undefinedfr/undefined-wp-framework/autoload.php';
Dépendances
- Timber ^2.0
- Extended ACF ^14.2
Structure recommandée
theme/
├── app/
│ ├── Block/ # Blocs ACF Gutenberg (legacy)
│ ├── blocks/ # Blocs Timber-style (nouveau)
│ ├── Command/ # Commandes WP-CLI
│ ├── PostType/ # Custom Post Types
│ ├── Taxonomy/ # Taxonomies personnalisées
│ ├── Controllers/ # Contrôleurs MVC
│ ├── Actions/ # Actions WordPress
│ ├── Filters/ # Filtres WordPress
│ └── Ajax/ # Fonctions Ajax
├── templates/ # Templates Twig
├── public/
│ └── assets/ # CSS, JS, images
└── functions.php
Custom Post Types
Créez vos CPT dans app/PostType/ en étendant PostType :
<?php namespace App\PostType; use Undefined\Core\PostType\PostType; class Project extends PostType { public static function getPostType(): string { return 'project'; } protected static function getPostTypeConfig(): array { return [ 'singulier' => 'Projet', 'pluriel' => 'Projets', 'feminin' => false, 'menu_icon' => 'dashicons-portfolio', 'supports' => ['title', 'editor', 'thumbnail'], 'taxonomies' => ['project_category'], ]; } // Hook appelé à la sauvegarde public static function onSavePost($post_id): void { // Logique personnalisée } }
Options de configuration
| Option | Type | Default | Description |
|---|---|---|---|
singulier |
string | ucfirst(slug) | Nom singulier |
pluriel |
string | singulier + 's' | Nom pluriel |
feminin |
bool | false | Genre féminin (labels FR) |
public |
bool | true | Accessible publiquement |
show_in_rest |
bool | true | API REST & Gutenberg |
menu_icon |
string | null | Dashicon ou SVG |
supports |
array | [...] | Fonctionnalités supportées |
taxonomies |
array | ['post_tag', 'category'] | Taxonomies associées |
rewrite |
string/array | slug | Règles de réécriture |
Custom Taxonomies
Créez vos taxonomies dans app/Taxonomy/ :
<?php namespace App\Taxonomy; use Undefined\Core\Taxonomy\Taxonomy; class ProjectCategory extends Taxonomy { public static function getTaxonomy(): string { return 'project_category'; } protected static function getTaxonomyConfig(): array { return [ 'name' => 'Catégorie', 'pluriel' => 'Catégories', 'feminin' => true, 'post_types' => ['project'], 'hierarchical' => true, ]; } // Hook appelé à la sauvegarde d'un terme public static function onSaveTerm($term_id, $tt_id, $update, $args): void { // Logique personnalisée } }
Blocs Gutenberg (ACF)
Structure Timber-style (recommandée)
app/blocks/
└── hero/
├── block.json # Métadonnées (optionnel)
├── hero.php # Classe du bloc
├── hero.twig # Template
├── hero.css # Styles (optionnel)
├── hero.js # Scripts (optionnel)
└── icon.svg # Icône (optionnel)
Exemple de bloc
<?php namespace App\blocks\hero; use Undefined\Core\Block\Block; use Extended\ACF\Fields\Text; use Extended\ACF\Fields\Image; class Hero extends Block { public $name = 'hero'; public $title = 'Hero Banner'; public $description = 'Bannière principale'; public $category = 'custom'; public $icon = 'cover-image'; public $mode = 'preview'; public $keywords = ['banner', 'header']; protected function _setGroupField(): void { parent::_setGroupField(); $this->groupField['fields'] = [ Text::make('Titre', 'title') ->required(), Text::make('Sous-titre', 'subtitle'), Image::make('Image', 'image') ->returnFormat('array'), ]; } // Données pour la preview dans l'éditeur protected function getExampleData(): array { return [ 'title' => 'Exemple de titre', 'subtitle' => 'Sous-titre', ]; } // Contexte additionnel pour le template protected function getContext(array $block, bool $is_preview, int $post_id): array { return [ 'custom_data' => 'value', ]; } }
Template Twig
{# app/blocks/hero/hero.twig #} <section class="hero-block" {% if block.anchor %}id="{{ block.anchor }}"{% endif %}> {% if block.data.title %} <h1>{{ block.data.title }}</h1> {% endif %} {% if block.data.subtitle %} <p>{{ block.data.subtitle }}</p> {% endif %} {% if block.data.image %} <img src="{{ block.data.image.url }}" alt="{{ block.data.image.alt }}"> {% endif %} </section>
Hooks disponibles
| Hook | Description |
|---|---|
undfnd_block_icon_paths |
Chemins de recherche pour l'icône |
undfnd_block_assets |
Assets CSS/JS du bloc |
undfnd_block_args |
Arguments d'enregistrement du bloc |
undfnd_block_prepared |
Données préparées du bloc |
undfnd_block_field_group |
Groupe de champs ACF |
undfnd_block_preview_template |
Template de preview vide |
Controllers & Routing
Créer un contrôleur
<?php // app/Controllers/AccountController.php use Undefined\Core\Controllers\AbstractController; class AccountController extends AbstractController { // Action par défaut: /account/ public function indexAction() { if (!is_user_logged_in()) { $this->_redirect('/login'); } $this->_setTitle('Mon compte'); $this->_setData([ 'user' => wp_get_current_user(), ]); $this->render(); } // Action: /account/orders public function ordersAction() { $this->_setData([ 'orders' => $this->getOrders(), ]); $this->render(); } // POST sur /account/orders public function ordersPostAction() { // Traitement du formulaire $this->_addNotice('success', 'Commande mise à jour'); $this->_redirect('/account/orders'); } }
Définir les routes
<?php // app/Router.php use Undefined\Core\Router; class AppRouter extends Router { public function __construct() { parent::__construct(); // Route simple $this->addRule('account'); // Route avec section $this->addRule('account', 'orders'); // Route avec paramètres dynamiques $this->addRule('account', 'order', [ 'order_id' => '([0-9]+)' ]); } }
Méthodes du contrôleur
| Méthode | Description |
|---|---|
render($data) |
Rend le template Twig |
_setTitle($title) |
Définit le titre de la page |
_setData($data, $merge) |
Ajoute des données au contexte |
_redirect($url) |
Redirige vers une URL |
_addNotice($type, $message) |
Ajoute une notification session |
getNotices() |
Récupère les notifications |
isCurrentRoute($route) |
Vérifie la route actuelle |
Request
Gestion des requêtes HTTP inspirée de Symfony :
use Undefined\Core\Request; $request = Request::createFromGlobals(); // Accès aux paramètres $request->query->get('page'); // $_GET['page'] $request->request->get('email'); // $_POST['email'] $request->cookies->get('session_id'); // $_COOKIE['session_id'] $request->server->get('REQUEST_METHOD'); $request->headers->get('Content-Type'); // Méthode générique (cherche dans attributes, query, request, session) $request->get('param', 'default'); // Contenu brut $request->getContent();
ParameterBag - Méthodes de sanitization
$request->request->getText('name'); // sanitize_text_field $request->request->getEmail('email'); // sanitize_email $request->request->getTextarea('bio'); // sanitize_textarea_field $request->request->getUrl('website'); // esc_url_raw $request->request->getFilename('file'); // sanitize_file_name $request->request->getKey('slug'); // sanitize_key $request->request->getSlug('title'); // sanitize_title // Méthodes standard $request->request->get('key', 'default'); $request->request->getInt('page', 1); $request->request->getBoolean('active'); $request->request->getAlpha('code'); $request->request->getAlnum('ref'); $request->request->getDigits('phone'); $request->request->has('field'); $request->request->all();
Actions & Filters
Actions
<?php namespace App\Actions; use Undefined\Core\Actions; class ThemeActions extends Actions { protected $_hooks = [ // Format simple 'setupTheme' => 'after_setup_theme', // Format avancé 'loadScripts' => [ 'hook' => 'wp_enqueue_scripts', 'priority' => 20, 'accepted_args' => 1, 'remove_on_admin' => true, ], ]; public function theme_setupTheme() { add_theme_support('post-thumbnails'); add_theme_support('title-tag'); register_nav_menus([ 'primary' => 'Menu principal', ]); } public function theme_loadScripts() { wp_enqueue_script('custom-script', '...'); } }
Filters
<?php namespace App\Filters; use Undefined\Core\Filters; class ThemeFilters extends Filters { protected $_hooks = [ 'excerpt' => 'excerpt_length', 'mimeTypes' => [ 'hook' => 'upload_mimes', 'priority' => 10, ], ]; public function theme_excerpt($length) { return 30; } public function theme_mimeTypes($mimes) { $mimes['svg'] = 'image/svg+xml'; return $mimes; } }
Ajax
<?php namespace App\Ajax; use Undefined\Core\Ajax; class FormAjax extends Ajax { protected $_ajaxFunctions = [ 'submit_contact_form', 'load_more_posts', ]; public function submit_contact_form() { check_ajax_referer('undefined_ajax_nonce', 'nonce'); $email = sanitize_email($_POST['email']); $message = sanitize_textarea_field($_POST['message']); // Traitement... wp_send_json_success(['message' => 'Email envoyé']); } public function load_more_posts() { $page = intval($_POST['page']); $posts = get_posts([ 'posts_per_page' => 10, 'offset' => ($page - 1) * 10, ]); wp_send_json_success(['posts' => $posts]); } }
Appel côté JavaScript
fetch(args.ajax_url + '?is_ajax=1', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'submit_contact_form', nonce: args.ajax_nonce, email: 'test@example.com', message: 'Hello' }) }) .then(response => response.json()) .then(data => console.log(data));
Assets
<?php namespace App\Assets; use Undefined\Core\Assets\Assets; class ThemeAssets extends Assets { protected $_scripts = [ 'vendor' => [ 'handle' => 'vendor', 'filename' => 'vendor.js', 'deps' => ['jquery'], 'version' => '1.0', 'infooter' => false, ], 'app' => [ 'handle' => 'app', 'filename' => 'app.js', 'deps' => ['vendor'], 'version' => '1.0', 'infooter' => true, 'args' => [], // Données localisées ], ]; protected $_styles = [ 'theme' => [ 'handle' => 'theme', 'filename' => 'theme.css', 'deps' => [], 'version' => '1.0', ], ]; }
Cache-busting avec hash.json
Créez public/assets/hash.json pour le versioning :
{
"app.js": "js/app.a1b2c3d4.js",
"theme.css": "css/theme.e5f6g7h8.css"
}
Hook pour le nom de l'objet localisé
add_filter('undfnd_assets_app_object_name', function($name) { return 'myApp'; // window.myApp au lieu de window.args });
Envoi d'emails avec templates Twig :
use Undefined\Core\Mails\Mail; Mail::init() ->to('client@example.com') ->cc('copy@example.com') ->bcc('hidden@example.com') ->from('Mon Site <noreply@example.com>') ->subject('Confirmation de commande #{{ order_id }}') ->template('emails/order-confirmation.twig', [ 'order_id' => 123, 'items' => $orderItems, 'total' => '99.00€', '_html_content' => '<strong>HTML non échappé</strong>', ]) ->attach('/path/to/invoice.pdf') ->send();
Variables globales disponibles
{{ blogname }}- Nom du site{{ home_url }}- URL du site{{ stylesheet_uri }}- URL du thème{{ blogdescription }}- Description du site
Template email
{# templates/emails/order-confirmation.twig #} <!DOCTYPE html> <html> <head> <style> body { font-family: Arial, sans-serif; } </style> </head> <body> <h1>Merci pour votre commande !</h1> <p>Commande #{{ order_id }}</p> <table> {% for item in items %} <tr> <td>{{ item.name }}</td> <td>{{ item.price }}</td> </tr> {% endfor %} </table> <p><strong>Total: {{ total }}</strong></p> </body> </html>
WP-CLI Commands
<?php namespace App\Command; class ImportCommand { public $name = 'import'; /** * Import des produits * * ## OPTIONS * * [--file=<file>] * : Fichier CSV à importer * * ## EXAMPLES * * wp import products --file=products.csv * * @param array $args * @param array $assoc_args */ public function products($args, $assoc_args) { $file = $assoc_args['file'] ?? 'import.csv'; \WP_CLI::log("Import depuis $file..."); // Logique d'import... \WP_CLI::success('Import terminé !'); } }
Usage :
wp import products --file=data.csv
Security
Le module Security désactive automatiquement l'endpoint REST /wp/v2/users pour éviter l'énumération des utilisateurs.
// Désactivé automatiquement : // GET /wp-json/wp/v2/users // GET /wp-json/wp/v2/users/1
App Singleton
Classe de base pour créer une instance unique de votre application :
<?php namespace App; use Undefined\Core\App; class MyApp extends App { protected function __construct() { // Initialisation } } // Usage $app = \App\MyApp::getInstance();
Helper global
// Accès rapide via la fonction app() $app = app();
Hooks & Filtres de référence
Blocs
| Hook | Args | Description |
|---|---|---|
undfnd_acf_blocks_path |
$path |
Chemin des blocs ACF legacy |
undfnd_gutenberg_blocks_path |
$path |
Chemin des blocs Timber |
undfnd_block_namespaces |
$namespaces |
Namespaces de recherche |
undfnd_block_icon_paths |
$paths, $block |
Chemins des icônes |
undfnd_block_assets |
$assets, $block |
Assets du bloc |
undfnd_block_args |
$args, $block |
Args d'enregistrement |
undfnd_block_prepared |
$block, $this |
Block préparé |
undfnd_block_field_group |
$group, $name |
Champs ACF |
CPT & Taxonomies
| Hook | Args | Description |
|---|---|---|
undfnd_cpt_save_post_priority |
$priority, $post_type |
Priorité du hook save |
undfnd_cpt_save_post_accepted_args |
$args, $post_type |
Nombre d'args |
Assets
| Hook | Args | Description |
|---|---|---|
undfnd_hash_assets_path |
$path |
Chemin du fichier hash.json |
undfnd_assets_{handle}_object_name |
$name |
Nom de l'objet JS |
Templating
| Hook | Args | Description |
|---|---|---|
timber_default_options |
$options |
Options par défaut |
timber_global_context_data |
$data |
Données globales |
undfnd_controller_wp_title_action |
$action |
Action pour wp_title |
Licence
GPL-3.0-or-later
Auteur
Nicolas RIVIERE - undefined.fr