Documentation menu
On this page
Guide · v1.x
NeoPHP
NeoPHP is an ultra modular PHP framework with no dependency other than PHP 8.2.
This guide creates a working application in a few minutes: installation, a first page, a database, a form, a login and the deployment.
Each feature has its own documentation in src/{components,packages,process}/Feature/Docs/v1.x/README.md.
Requirements
- PHP 8.2 or higher
- Composer
- The PHP extensions of the features you use:
pdo_mysql,pdo_pgsqlorpdo_sqlite(database),openssl(SMTP over TLS, Tailwind download),fileinfo(uploads),curl(recommended for the HTTP client, parallel requests),apcu(optional,apcucache adapter),dom(XML format of the Serializer) - Optional:
twig/twig^3.0 for Twig templates
Create the project
Create the application from the neophp/skeleton project:
composer create-project neophp/skeleton my-app
cd my-app
php bin/neo serve
The skeleton composer.json:
{
"name": "neophp/skeleton",
"description": "NeoPHP application skeleton.",
"type": "project",
"license": "MIT",
"keywords": ["neophp", "skeleton", "framework"],
"require": {
"php": ">=8.2",
"neophp/framework": "^1.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"scripts": {
"neo:install": "@php vendor/bin/neo install",
"post-create-project-cmd": "@neo:install"
},
"config": {
"sort-packages": true
},
"minimum-stability": "stable",
"prefer-stable": true
}
composer create-project runs neo install, which generates the project files, a random APP_SECRET and APP_URL in .env. Existing files are never overwritten (php bin/neo install --force to regenerate them). Open http://127.0.0.1:8000.
For Twig templates, add Twig to the project:
composer require twig/twig
Project structure
assets/ CSS, JS and images compiled into public/builds/
bin/neo command line
config/
framework/ configuration of the components (api.yaml, app.yaml, cache.yaml, database.yaml, http_client.yaml, mailer.yaml, serializer.yaml, view.yaml...)
packages/ configuration of the packages (orm.yaml, security.yaml, debug.yaml, tailwind.yaml, translation.yaml)
routes.yaml routes
services.yaml services
migrations/ database migrations
public/index.php front controller
src/
Controller/ controllers
Entity/ ORM entities
Repository/ repositories
Form/ forms
Command/ console commands
Kernel.php
templates/ PHP (.php) and Twig (.html.twig) templates
translations/ translation files ({domain}.{locale}.yaml|xlf)
var/ cache, logs, sessions
.env environment variables (APP_ENV, APP_DEBUG, APP_SECRET, APP_URL, DATABASE_URL, MAILER_DSN)
First page
src/Controller/HelloController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NeoPHP\Component\Controller\Contract\AbstractController;
use NeoPHP\Component\Http\Response\Response;
use NeoPHP\Component\Routing\Attribute\Route;
class HelloController extends AbstractController
{
#[Route('/hello/{name}', name: 'hello', methods: ['GET'], defaults: ['name' => 'world'])]
public function index(string $name): Response
{
return $this->render('hello/index.html.twig', ['name' => $name]);
}
}
templates/hello/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<h1>Hello {{ name }}!</h1>
<a href="{{ path('hello', {name: 'NeoPHP'}) }}">Say hello to NeoPHP</a>
{% endblock %}
The same page with a PHP template, templates/hello/index.php:
<?php $this->extend('base') ?>
<?php $this->start('content') ?>
<h1>Hello <?= $this->e($name) ?>!</h1>
<?php $this->stop() ?>
Controllers are discovered in src/Controller/ (#[Route]), or declared in config/routes.yaml. See the Routing, Controller and View documentation.
Database and entities
.env
DATABASE_URL="mysql://user:password@127.0.0.1:3306/app?charset=utf8mb4"
php bin/neo db:create --if-not-exists
php bin/neo make:entity
make:entity is a wizard: it asks the entity name, then the fields one by one (type ? to list the types), the relations and their inverse side. Then:
php bin/neo make:migration
php bin/neo migration:migrate
In a controller:
#[Route('/posts', name: 'post_index')]
public function index(PostRepository $posts): Response
{
return $this->render('post/index.html.twig', ['posts' => $posts->findBy([], ['id' => 'DESC'])]);
}
See the Database and ORM documentation.
Forms
php bin/neo make:form Post Post
#[Route('/posts/new', name: 'post_new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
$post = new Post();
$form = $this->createForm(PostForm::class, $post);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getOrm()->persist($post);
$this->getOrm()->flush();
$this->addFlash('success', 'Post created.');
return $this->redirectToRoute('post_index');
}
return $this->render('post/new.html.twig', ['form' => $form]);
}
{{ form(form) }}
The CSRF token is added automatically. Set theme: bootstrap5 in config/framework/form.yaml for Bootstrap 5. See the Form, Validator and Csrf documentation.
APIs
#[Route('/api/posts', name: 'api_post_create', methods: ['POST'])]
public function create(#[MapRequestPayload] PostInput $input): JsonResponse
{
$post = (new Post())->setTitle($input->title)->setContent($input->content);
$this->getOrm()->persist($post);
$this->getOrm()->flush();
return $this->json($post, 201, [], ['groups' => ['read']]);
}
The JSON (or XML, form) body is decoded, mapped to the PostInput DTO and validated: invalid data returns a 422 JSON response with the violations, a malformed body a 400 and an unsupported Content-Type a 415. json() normalizes objects with the Serializer (#[Groups], #[SerializedName], #[Ignore]...). See the Serializer documentation.
The Api component adds the rest of an API toolkit, configured in config/framework/api.yaml:
#[Route('/api/posts', name: 'api_post_index', methods: ['GET'])]
#[RateLimit('api')]
#[OA\Response(200, type: Post::class, groups: ['read'], paginated: true)]
public function index(PostRepository $posts, #[MapPagination] PageRequest $pageRequest): JsonResponse
{
$page = $this->paginate($posts->createQueryBuilder('p')->orderBy('p.id', 'DESC'), $pageRequest);
return $this->jsonPage($page, ['groups' => ['read']]);
}
- CORS: preflight
OPTIONSrequests answered before routing, headers added to every response of the configured paths (also errors),#[Cors]per controller; - rate limiting: named limiters (fixed window, sliding window, token bucket) stored in a cache pool,
#[RateLimit('api')]or$this->rateLimit('login', $email), 429 withRetry-AfterandX-RateLimit-*headers; - pagination:
?page=&limit=for arrays, iterables and ORM query builders,{"items", "pagination", "links"}withLinkandX-Total-Countheaders; - RFC 7807 problem details: errors of
/apiroutes rendered asapplication/problem+json(validation violations included); - OpenAPI 3.1:
php bin/neo openapi:dumpor/api/docgenerated from the routes, DTOs, serializer groups and validation constraints.
See the Api documentation.
Login
php bin/neo make:user
php bin/neo make:migration && php bin/neo migration:migrate
php bin/neo make:auth --twig
config/packages/security.yaml
providers:
users:
entity:
class: App\Entity\User
property: email
firewalls:
main:
pattern: ^/
provider: users
form_login:
login_path: app_login
enable_csrf: true
logout:
path: app_logout
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
In controllers: $this->getUser(), $this->denyAccessUnlessGranted('ROLE_ADMIN'), #[IsGranted('ROLE_ADMIN')]. In templates: app_user(), is_granted('ROLE_ADMIN'), logout_path(). See the Security documentation.
Styles with Tailwind
php bin/neo tailwind:install
php bin/neo tailwind:run --watch
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
Without Tailwind, the files of assets/ are served with asset() as well. See the Asset and Tailwind documentation.
Translations
Enable the locales in config/packages/translation.yaml (locales: [en, fr]) and write translations/messages.fr.yaml:
home:
title: Bienvenue
posts: "{count, plural, =0 {Aucun article} one {# article} other {# articles}}"
<h1>{{ 'home.title'|trans }}</h1>
<p>{{ translate('home.posts', {count: posts|length}) }}</p>
The locale is detected from the route {_locale}, ?lang=, the session, a cookie or Accept-Language; $this->switchLocale('fr') in a controller remembers it. Validation and login errors are written in English and translated through translations/validators.fr.yaml and translations/security.fr.yaml. php bin/neo translation:generate adds the missing keys to the files. See the Translation documentation.
Useful commands
| Command | Description |
|---|---|
php bin/neo serve |
development server |
php bin/neo list |
every command |
php bin/neo route:list |
routes |
php bin/neo make:entity (also make:form, make:command, make:user, make:auth, make:voter, make:email) |
code generators (they ask the missing values) |
php bin/neo make:migration / migration:migrate |
database migrations |
php bin/neo cache:clear |
clears var/cache/ |
php bin/neo cache:pool:clear --all |
clears the cache pools (also cache:pool:list, cache:pool:prune) |
php bin/neo serializer:debug "App\Entity\Post" |
serialization metadata of a class |
php bin/neo asset:reload --minify |
compiles assets/ into public/builds/ |
php bin/neo openapi:dump --format=yaml |
OpenAPI document of the API routes |
php bin/neo debug:container |
services of the container |
php bin/neo translation:generate / translation:debug / translation:lint |
translation files |
php bin/neo help <command> shows the options and examples of a command. See the Console documentation.
Deployment
.env.local(or real environment variables):APP_ENV=prod,APP_DEBUG=0,APP_URL=https://example.com,DATABASE_URL,MAILER_DSNcomposer install --no-dev --optimize-autoloaderphp bin/neo migration:migrate -nphp bin/neo tailwind:run --minify(if Tailwind is used), thenphp bin/neo asset:reload --minifyphp bin/neo cache:clear- Point the web server document root to
public/(public/.htaccessis provided for Apache)
Features
| Group | Features |
|---|---|
| components | Asset, Cache, Config, Container, Controller, Cookie, Csrf, Database, Event, Exception, Flash, Form, Http, HttpClient, Kernel, Logger, Mailer, Middleware, Routing, Serializer, Service, Session, Validator, View |
| packages | Debug, Dotenv, Markdown, Orm, Security, Tailwind, Translation, Yaml |
| process | Console, Installer |
The documentation of a feature is in src/<group>/<Feature>/Docs/v1.x/README.md, for example src/components/Routing/Docs/v1.x/README.md.
Changelog
- v1.29.0 — NeoAI package (development assistant enabled only in debug: OpenAI, OpenAI-compatible (Mistral, Groq, OpenRouter, LM Studio, vLLM), Anthropic, Gemini and Ollama providers, read-only
neo-toolloop in a sandbox with secret redaction,ai:startchat with patch review,ai:scanaudit with Markdown report,ai:test, toolbar chat and profiler panel,config/packages/neo_ai.yaml); WebProfilerToolbarAssetInterfaceto add CSS / JavaScript to the toolbar - v1.28.2 — Bugfix: translation profiler
- v1.28.1 — Bugfix: missing
translation:generatecommand - v1.28.0 — Translation profiler (toolbar item and panel, opt-in
TranslationTrace: defined / fallback / missing messages, locale detection source, loaded catalogues) - v1.27.0 — Security profiler (toolbar item and panel, opt-in
SecurityTrace: access decisions, voter votes,access_control, login / logout events) - v1.26.0 — Database and ORM profiler (query logger with transactions, Database panel, unit of work statistics, ORM panel)
- v1.25.2 — Bugfix: WebProfiler controller helper location
- v1.25.1 — Bugfix: YAML indentation of the generated configuration files
- v1.25.0 — WebProfiler package (web debug toolbar and
/_profilerinterface, elements discovered inHelper/Profilerof every feature and with#[AsProfiler], panels and blocks,Stopwatch, Ajax requests tracking,profiler:listandprofiler:clear,config/packages/web_profiler.yaml) - v1.24.0 — Api component (CORS, rate limiter with fixed window / sliding window / token bucket policies,
#[RateLimit], pagination withLink/X-Total-Countheaders and#[MapPagination], RFC 7807 problem details, OpenAPI 3.1 generation with#[OA\Operation]/#[OA\Response]/#[OA\Tag],openapi:dump,/api/doc),TooManyRequestsHttpException - v1.23.0 — Serializer component (JSON / XML / CSV / YAML, normalizers for objects, dates, enums and ORM entities,
#[Groups],#[SerializedName],#[Ignore],#[MaxDepth],#[Context],#[Type],#[MapRequestPayload]/#[MapQueryString]controller arguments,json()with a serializer context,serialize()in controllers,serializer:debug) - v1.22.0 — Cache component (pools with filesystem / APCu / database / array adapters,
get()with callback and stampede protection, tags,cache()in controllers,cache:pool:*commands, HttpClientcacheoption) - v1.21.0 — HttpClient component (requests with JSON / form / multipart bodies, curl and stream transports, parallel requests, downloads, retries, named clients,
httpClient(),http:request) - v1.20.0 — Translation package (YAML / XLIFF catalogues, ICU-lite plurals, locale detection,
translate()/trans, translated validation and security messages,translation:generate,translation:debug,translation:lint) - v1.19.0 — Markdown package (parser, document API, HTML to Markdown,
markdownfilter,markdown:convert) - v1.18.0 — Tailwind package (
tailwind:install,tailwind:run) - Bugfix after v1.17.0 — absolute URLs (
url(),APP_URL),make:authbase layout,make:migrationdescription, misnamed view helpers reported in debug - v1.17.0 — interactive console,
make:entitywizard - v1.16.0 — Mailer
- v1.15.0 — Console refactor (
#[AsCommand],AbstractConsole) - v1.14.0 — Debug (
dump(),dd()) - v1.13.0 — Security
- v1.12.0 — Forms and CSRF
- v1.11.0 — Database and ORM
- v1.10.0 — Validator
- v1.9.0 — Events (v1.9.1: routing uses the kernel class discovery)
- v1.8.0 —
#[Autowire],#[Inject],config/services.yaml - v1.7.0 — Middlewares
- v1.6.0 — Session, cookies and flash messages
- v1.5.0 —
#[Route]attributes, routes cache - v1.4.0 —
AbstractControllermade of feature traits - v1.3.0 — Assets
- v1.2.0 — Twig and view helpers
- v1.1.0 — Logger
- v1.0.0 — Base: routes, YAML, views, controllers, container, HTTP, console, installer, configuration