Todo-Backend example implementation with Symfony 6 and api-platform
Table of Contents
- 1. Test locally
- 2. Motivation
- 3. History
- 4. How it was built
- 4.1. Initialisation of the project
- 4.2. Adding the model of the Todo application with Doctrine
- 4.3. Add API Platform
- 4.4. Tweaking compliance with the TodoBackend API
- 4.4.1. Installing the test suite and running it
- 4.4.2. Fixing the "application/json" type not supported for POST
- 4.4.3. Adding default value for the 'completed' attribute
- 4.4.4. Adding DELETE on collection
- 4.4.5. Changing default content-type produced to JSON
- 4.4.6. Adding an url property for the Todo items
- 4.4.7. Supporting PATCH request on Todo items
- 4.4.8. Fixing the order naming
- 5. Deploying
This repository contains a Todo-Backend example implementation made with Symfony 6 (LTS) using the api-platform project.
The current documentation is best viewed in https://todobackend-symfony6-c029e2.gitlab.io/README.html.
1. Test locally
Clone the project's Git repo (see https://gitlab.com/olberger/todobackend-symfony6), and start it on port 8000, for instance :
php -S 127.0.0.1:8000 -t public
Then connect to http://localhost:8000/ or http://localhost:8000/todos in your browser. The app serves HTML if requested, which documents its use, giving example requests for use with cURL for instance. RTFM ;-)
You can then check the compatibility with the TodoBackend test suite :
1.1. Passing TodoBackend tests
You can test for todo-backend API compliance, by cloning the repository's code from https://github.com/TodoBackend/todo-backend-js-spec.
You may then test using :
cd todo-backend-js-spec
php -S localhost:8080
You can then connect to http://localhost:8080/?http://127.0.0.1:8000/api/todos in your browser.
2. Motivation
We've been devising the teaching materials for a class on Web apps development, taught in PHP with Symfony.
I thought it would be great to illustrate the course with some ToDo app which could be compared to other implementations, and found the TodoBackend project.
Instead of just doing some implementation for our colleagues and students, I thought I could as well do one that could be useful to others. Also, this took me hours to find the most suitable ways to make it work with Symfony 6 and api-platform.
3. History
A previous version developped for Symfony 4 may be seen at https://gitlab.com/olberger/todobackend-symfony4.
Note that there used to be https://github.com/oegnus/symfony2-todobackend for an older variant of Symfony, which I used for inspiration.
4. How it was built
Here are some very raw notes I took when implementing it.
4.1. Initialisation of the project
No need for full-fledged web app:
composer --no-ansi create-project symfony/skeleton:"6.4.*" todobackend-symfony6
Creating a "symfony/skeleton:6.4.*" project at "./todobackend-symfony6" Installing symfony/skeleton (v6.4.99) - Installing symfony/skeleton (v6.4.99): Extracting archive Created project in /home/olivier/git/gitlab.com/olberger/tests-todobackends/todobackend-symfony6 Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - Locking symfony/flex (v2.8.1) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals - Installing symfony/flex (v2.8.1): Extracting archive Generating autoload files 1 package you are using is looking for funding. Use the `composer fund` command to find out more! Run composer recipes at any time to see the status of your Symfony recipes. Loading composer repositories with package information Restricting packages listed in "symfony/symfony" to "6.4.*" Updating dependencies Lock file operations: 30 installs, 0 updates, 0 removals - Locking psr/cache (3.0.0) - Locking psr/container (2.0.2) - Locking psr/event-dispatcher (1.0.0) - Locking psr/log (3.0.2) - Locking symfony/cache (v6.4.24) - Locking symfony/cache-contracts (v3.6.0) - Locking symfony/config (v6.4.24) - Locking symfony/console (v6.4.24) - Locking symfony/dependency-injection (v6.4.24) - Locking symfony/deprecation-contracts (v3.6.0) - Locking symfony/dotenv (v6.4.24) - Locking symfony/error-handler (v6.4.24) - Locking symfony/event-dispatcher (v6.4.24) - Locking symfony/event-dispatcher-contracts (v3.6.0) - Locking symfony/filesystem (v6.4.24) - Locking symfony/finder (v6.4.24) - Locking symfony/framework-bundle (v6.4.24) - Locking symfony/http-foundation (v6.4.24) - Locking symfony/http-kernel (v6.4.24) - Locking symfony/polyfill-intl-grapheme (v1.32.0) - Locking symfony/polyfill-intl-normalizer (v1.32.0) - Locking symfony/polyfill-mbstring (v1.32.0) - Locking symfony/polyfill-php83 (v1.32.0) - Locking symfony/routing (v6.4.24) - Locking symfony/runtime (v6.4.24) - Locking symfony/service-contracts (v3.6.0) - Locking symfony/string (v6.4.24) - Locking symfony/var-dumper (v6.4.24) - Locking symfony/var-exporter (v6.4.24) - Locking symfony/yaml (v6.4.24) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 30 installs, 0 updates, 0 removals - Installing symfony/runtime (v6.4.24): Extracting archive - Installing psr/cache (3.0.0): Extracting archive - Installing symfony/cache-contracts (v3.6.0): Extracting archive - Installing symfony/polyfill-mbstring (v1.32.0): Extracting archive - Installing symfony/polyfill-intl-normalizer (v1.32.0): Extracting archive - Installing symfony/polyfill-intl-grapheme (v1.32.0): Extracting archive - Installing symfony/string (v6.4.24): Extracting archive - Installing symfony/deprecation-contracts (v3.6.0): Extracting archive - Installing psr/container (2.0.2): Extracting archive - Installing symfony/service-contracts (v3.6.0): Extracting archive - Installing symfony/console (v6.4.24): Extracting archive - Installing symfony/dotenv (v6.4.24): Extracting archive - Installing psr/event-dispatcher (1.0.0): Extracting archive - Installing symfony/event-dispatcher-contracts (v3.6.0): Extracting archive - Installing symfony/routing (v6.4.24): Extracting archive - Installing symfony/polyfill-php83 (v1.32.0): Extracting archive - Installing symfony/http-foundation (v6.4.24): Extracting archive - Installing symfony/event-dispatcher (v6.4.24): Extracting archive - Installing symfony/var-dumper (v6.4.24): Extracting archive - Installing psr/log (3.0.2): Extracting archive - Installing symfony/error-handler (v6.4.24): Extracting archive - Installing symfony/http-kernel (v6.4.24): Extracting archive - Installing symfony/finder (v6.4.24): Extracting archive - Installing symfony/filesystem (v6.4.24): Extracting archive - Installing symfony/var-exporter (v6.4.24): Extracting archive - Installing symfony/dependency-injection (v6.4.24): Extracting archive - Installing symfony/config (v6.4.24): Extracting archive - Installing symfony/cache (v6.4.24): Extracting archive - Installing symfony/framework-bundle (v6.4.24): Extracting archive - Installing symfony/yaml (v6.4.24): Extracting archive Generating autoload files 27 packages you are using are looking for funding. Use the `composer fund` command to find out more! Symfony operations: 4 recipes (4b97d49db602544762d53fd416ec9767) - Configuring symfony/flex (>=2.4): From github.com/symfony/recipes:main - Configuring symfony/framework-bundle (>=6.4): From github.com/symfony/recipes:main - Configuring symfony/console (>=5.3): From github.com/symfony/recipes:main - Configuring symfony/routing (>=6.2): From github.com/symfony/recipes:main Executing script cache:clear [OK] Executing script assets:install public [OK] What's next? Some files have been created and/or updated to configure your new packages. Please review, edit and commit them: these files are yours. symfony/framework-bundle instructions: * Run your application: 1. Go to the project directory 2. Create your code repository with the git init command 3. Download the Symfony CLI at https://symfony.com/download to install a development web server * Read the documentation at https://symfony.com/doc No security vulnerability advisories found. No security vulnerability advisories found.
Note you may do the same withouth the –no-ansi, which is used here for documentation generation purposes.
The generated project is testable with:
cd todobackend-symfony6/
php -S localhost:8000 -t public
Cf. http://localhost:8000 for the default Symfony page.
4.2. Adding the model of the Todo application with Doctrine
Cf. https://symfony.com/doc/current/doctrine.html for docs explaining the following.
add the
doctrine
flex recipe:composer --no-ansi require doctrine
./composer.json has been updated Running composer update symfony/orm-pack Loading composer repositories with package information Restricting packages listed in "symfony/symfony" to "6.4.*" Updating dependencies Lock file operations: 17 installs, 0 updates, 0 removals - Locking doctrine/collections (2.3.0) - Locking doctrine/dbal (3.10.1) - Locking doctrine/deprecations (1.1.5) - Locking doctrine/doctrine-bundle (2.15.1) - Locking doctrine/doctrine-migrations-bundle (3.4.2) - Locking doctrine/event-manager (2.0.1) - Locking doctrine/inflector (2.1.0) - Locking doctrine/instantiator (2.0.0) - Locking doctrine/lexer (3.0.1) - Locking doctrine/migrations (3.9.3) - Locking doctrine/orm (3.5.2) - Locking doctrine/persistence (4.0.0) - Locking doctrine/sql-formatter (1.5.2) - Locking symfony/doctrine-bridge (v6.4.24) - Locking symfony/orm-pack (v2.4.1) - Locking symfony/polyfill-php84 (v1.32.0) - Locking symfony/stopwatch (v6.4.24) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 17 installs, 0 updates, 0 removals - Downloading doctrine/migrations (3.9.3) - Installing symfony/polyfill-php84 (v1.32.0): Extracting archive - Installing doctrine/deprecations (1.1.5): Extracting archive - Installing doctrine/collections (2.3.0): Extracting archive - Installing doctrine/inflector (2.1.0): Extracting archive - Installing doctrine/instantiator (2.0.0): Extracting archive - Installing doctrine/lexer (3.0.1): Extracting archive - Installing symfony/stopwatch (v6.4.24): Extracting archive - Installing doctrine/event-manager (2.0.1): Extracting archive - Installing doctrine/dbal (3.10.1): Extracting archive - Installing doctrine/migrations (3.9.3): Extracting archive - Installing doctrine/sql-formatter (1.5.2): Extracting archive - Installing doctrine/persistence (4.0.0): Extracting archive - Installing symfony/doctrine-bridge (v6.4.24): Extracting archive - Installing doctrine/orm (3.5.2): Extracting archive - Installing doctrine/doctrine-bundle (2.15.1): Extracting archive - Installing doctrine/doctrine-migrations-bundle (3.4.2): Extracting archive - Installing symfony/orm-pack (v2.4.1) Generating autoload files 41 packages you are using are looking for funding. Use the `composer fund` command to find out more! Symfony operations: 3 recipes (6badd6a9863edf36749f3c768fcc2d86) - Configuring doctrine/deprecations (>=1.0): From github.com/symfony/recipes:main - Configuring doctrine/doctrine-bundle (>=2.10): From github.com/symfony/recipes:main - WARNING doctrine/doctrine-bundle (>=2.10): From github.com/symfony/recipes:main The recipe for this package contains some Docker configuration. This may create/update compose.yaml or update Dockerfile (if it exists). Do you want to include Docker configuration from recipes? [y] Yes [n] No [p] Yes permanently, never ask again for this project [x] No permanently, never ask again for this project (defaults to y): - Configuring doctrine/doctrine-migrations-bundle (>=3.1): From github.com/symfony/recipes:main - Unpacked symfony/orm-pack Executing script cache:clear [OK] Executing script assets:install public [OK] What's next? Some files have been created and/or updated to configure your new packages. Please review, edit and commit them: these files are yours. doctrine/doctrine-bundle instructions: * Modify your DATABASE_URL config in .env * Configure the driver (postgresql) and server_version (16) in config/packages/doctrine.yaml No security vulnerability advisories found.
As advised, change the
DATABASE_URL
config in the.env
file to use SQLite (see results in .env) :--- ../tests-todobackends/todobackend-symfony6/.env 2025-08-14 16:41:04.023566646 +0200 +++ .env 2025-08-13 15:02:37.254222718 +0200 @@ -26,5 +26,6 @@ APP_SECRET=6516abcf9ebc16f08291cea3e806e200 # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" ###< doctrine/doctrine-bundle ###
Then create the database :
bin/console --no-ansi doctrine:database:create
Created database /home/olivier/git/gitlab.com/olberger/tests-todobackends/todobackend-symfony6/var/data_dev.db for connection named default
Then use the maker tool to add a
Todo
entity with its properties to the application model:composer --no-ansi require maker --dev
./composer.json has been updated Running composer update symfony/maker-bundle Loading composer repositories with package information Restricting packages listed in "symfony/symfony" to "6.4.*" Updating dependencies Lock file operations: 3 installs, 0 updates, 0 removals - Locking nikic/php-parser (v5.6.1) - Locking symfony/maker-bundle (v1.64.0) - Locking symfony/process (v6.4.24) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 3 installs, 0 updates, 0 removals - Downloading nikic/php-parser (v5.6.1) - Installing symfony/process (v6.4.24): Extracting archive - Installing nikic/php-parser (v5.6.1): Extracting archive - Installing symfony/maker-bundle (v1.64.0): Extracting archive Generating autoload files 42 packages you are using are looking for funding. Use the `composer fund` command to find out more! Symfony operations: 1 recipe (48a97bb175ee23b2d0355875a7a6af42) - Configuring symfony/maker-bundle (>=1.0): From github.com/symfony/recipes:main Executing script cache:clear [OK] Executing script assets:install public [OK] What's next? Some files have been created and/or updated to configure your new packages. Please review, edit and commit them: these files are yours. No security vulnerability advisories found. Using version ^1.64 for symfony/maker-bundle
Then use the maker helper to create the model entity (
Todo
). Pay attention to the renaming needed for the order attribute which is a reserved keyword:php bin/console make:entity
Class name of the entity to create or update (e.g. FierceKangaroo): > Todo created: src/Entity/Todo.php created: src/Repository/TodoRepository.php Entity generated! Now let's add some fields! You can always add more fields later manually or by re-running this command. New property name (press <return> to stop adding fields): > title Field type (enter ? to see all types) [string]: > Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Todo.php Add another property? Enter the property name (or press <return> to stop adding fields): > completed Field type (enter ? to see all types) [string]: > boolean Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Todo.php Add another property? Enter the property name (or press <return> to stop adding fields): > order [ERROR] Name "order" is a reserved word. Add another property? Enter the property name (or press <return> to stop adding fields): > todo_order Field type (enter ? to see all types) [string]: > integer Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Todo.php Add another property? Enter the property name (or press <return> to stop adding fields): > Success! Next: When you're ready, create a migration with make:migration
Note that the
order
attribute isn't allowed by Doctrine, so we'll call ittodo_order
instead. Later, we'll adjust this so that the API recognizes it asorder
by changing getters and setters operationsThis generates a
src/Entity/Todo.php
file with the corresponding@ORM
annotations
Create the database tables:
bin/console --no-ansi doctrine:schema:create
4.3. Add API Platform
Install the api-platform (https://github.com/api-platform/api-platform)
composer --no-ansi req api
./composer.json has been updated Running composer update api-platform/api-pack Loading composer repositories with package information Restricting packages listed in "symfony/symfony" to "6.4.*" Updating dependencies Lock file operations: 46 installs, 0 updates, 0 removals - Locking api-platform/api-pack (v1.4.0) - Locking api-platform/doctrine-common (v4.1.20) - Locking api-platform/doctrine-orm (v4.1.20) - Locking api-platform/documentation (v4.1.20) - Locking api-platform/http-cache (v4.1.20) - Locking api-platform/hydra (v4.1.20) - Locking api-platform/json-schema (v4.1.20) - Locking api-platform/jsonld (v4.1.20) - Locking api-platform/metadata (v4.1.20) - Locking api-platform/openapi (v4.1.20) - Locking api-platform/serializer (v4.1.20) - Locking api-platform/state (v4.1.20) - Locking api-platform/symfony (v4.1.20) - Locking api-platform/validator (v4.1.20) - Locking doctrine/common (3.5.0) - Locking nelmio/cors-bundle (2.5.0) - Locking phpdocumentor/reflection-common (2.2.0) - Locking phpdocumentor/reflection-docblock (5.6.2) - Locking phpdocumentor/type-resolver (1.10.0) - Locking phpstan/phpdoc-parser (2.2.0) - Locking psr/clock (1.0.0) - Locking psr/link (2.0.1) - Locking symfony/asset (v6.4.24) - Locking symfony/clock (v6.4.24) - Locking symfony/expression-language (v6.4.24) - Locking symfony/orm-pack (v2.4.1) - Locking symfony/password-hasher (v6.4.24) - Locking symfony/polyfill-uuid (v1.32.0) - Locking symfony/property-access (v6.4.24) - Locking symfony/property-info (v6.4.24) - Locking symfony/security-bundle (v6.4.24) - Locking symfony/security-core (v6.4.24) - Locking symfony/security-csrf (v6.4.24) - Locking symfony/security-http (v6.4.24) - Locking symfony/serializer (v6.4.24) - Locking symfony/serializer-pack (v1.3.0) - Locking symfony/translation-contracts (v3.6.0) - Locking symfony/twig-bridge (v6.4.24) - Locking symfony/twig-bundle (v6.4.24) - Locking symfony/type-info (v7.3.2) - Locking symfony/uid (v6.4.24) - Locking symfony/validator (v6.4.24) - Locking symfony/web-link (v6.4.24) - Locking twig/twig (v3.21.1) - Locking webmozart/assert (1.11.0) - Locking willdurand/negotiation (3.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 46 installs, 0 updates, 0 removals - Installing symfony/translation-contracts (v3.6.0): Extracting archive - Installing symfony/validator (v6.4.24): Extracting archive - Installing twig/twig (v3.21.1): Extracting archive - Installing symfony/twig-bridge (v6.4.24): Extracting archive - Installing symfony/twig-bundle (v6.4.24): Extracting archive - Installing symfony/serializer (v6.4.24): Extracting archive - Installing symfony/property-info (v6.4.24): Extracting archive - Installing symfony/property-access (v6.4.24): Extracting archive - Installing phpstan/phpdoc-parser (2.2.0): Extracting archive - Installing webmozart/assert (1.11.0): Extracting archive - Installing phpdocumentor/reflection-common (2.2.0): Extracting archive - Installing phpdocumentor/type-resolver (1.10.0): Extracting archive - Installing phpdocumentor/reflection-docblock (5.6.2): Extracting archive - Installing symfony/serializer-pack (v1.3.0) - Installing symfony/password-hasher (v6.4.24): Extracting archive - Installing symfony/security-core (v6.4.24): Extracting archive - Installing symfony/security-http (v6.4.24): Extracting archive - Installing symfony/security-csrf (v6.4.24): Extracting archive - Installing psr/clock (1.0.0): Extracting archive - Installing symfony/clock (v6.4.24): Extracting archive - Installing symfony/security-bundle (v6.4.24): Extracting archive - Installing symfony/orm-pack (v2.4.1) - Installing symfony/expression-language (v6.4.24): Extracting archive - Installing symfony/asset (v6.4.24): Extracting archive - Installing nelmio/cors-bundle (2.5.0): Extracting archive - Installing willdurand/negotiation (3.1.0): Extracting archive - Installing psr/link (2.0.1): Extracting archive - Installing symfony/web-link (v6.4.24): Extracting archive - Installing symfony/type-info (v7.3.2): Extracting archive - Installing api-platform/metadata (v4.1.20): Extracting archive - Installing api-platform/validator (v4.1.20): Extracting archive - Installing api-platform/state (v4.1.20): Extracting archive - Installing api-platform/serializer (v4.1.20): Extracting archive - Installing symfony/polyfill-uuid (v1.32.0): Extracting archive - Installing symfony/uid (v6.4.24): Extracting archive - Installing api-platform/json-schema (v4.1.20): Extracting archive - Installing api-platform/openapi (v4.1.20): Extracting archive - Installing api-platform/jsonld (v4.1.20): Extracting archive - Installing api-platform/documentation (v4.1.20): Extracting archive - Installing api-platform/hydra (v4.1.20): Extracting archive - Installing api-platform/http-cache (v4.1.20): Extracting archive - Installing api-platform/symfony (v4.1.20): Extracting archive - Installing doctrine/common (3.5.0): Extracting archive - Installing api-platform/doctrine-common (v4.1.20): Extracting archive - Installing api-platform/doctrine-orm (v4.1.20): Extracting archive - Installing api-platform/api-pack (v1.4.0) Generating autoload files 65 packages you are using are looking for funding. Use the `composer fund` command to find out more! Symfony operations: 6 recipes (b1b32769d05247584e33158a380da54d) - Configuring symfony/validator (>=5.3): From github.com/symfony/recipes:main - Configuring symfony/twig-bundle (>=6.4): From github.com/symfony/recipes:main - Configuring symfony/security-bundle (>=6.4): From github.com/symfony/recipes:main - Configuring nelmio/cors-bundle (>=1.5): From github.com/symfony/recipes:main - Configuring symfony/uid (>=6.2): From github.com/symfony/recipes:main - Configuring api-platform/symfony (>=4.0): From github.com/symfony/recipes:main - Unpacked api-platform/api-pack - Unpacked symfony/orm-pack - Unpacked symfony/serializer-pack Executing script cache:clear [OK] Executing script assets:install public [OK] What's next? Some files have been created and/or updated to configure your new packages. Please review, edit and commit them: these files are yours. api-platform/symfony instructions: * Your API is almost ready: 1. Create your first API resource in src/ApiResource; 2. Go to /api to browse your API * Using MakerBundle? Try php bin/console make:entity --api-resource * To enable the GraphQL support, run composer require api-platform/graphql, then browse /api/graphql. * Read the documentation at https://api-platform.com/docs/ No security vulnerability advisories found.
Then declare the
Todo
entities as to be handled through the API:@@ -2,10 +2,12 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; use App\Repository\TodoRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: TodoRepository::class)] +#[ApiResource] class Todo { #[ORM\Id]
You may then test the API at: http://127.0.0.1:8000/api in your browser
Here's the JSON-LD produced by default on the empty database:
curl -s -X GET "http://127.0.0.1:8000/api/todos" -H "accept: application/ld+json" | jq -M
{ "@context": "/api/contexts/Todo", "@id": "/api/todos", "@type": "Collection", "totalItems": 0, "member": [] }
That's it for the working API-Platform default features.
4.4. Tweaking compliance with the TodoBackend API
The goal will be to test that the test suite works, for instance with http://www.todobackend.com/specs/index.html?http://127.0.0.1:8000/api/todos
But that requires CORS support, so we'll test locally first
4.4.1. Installing the test suite and running it
See Passing TodoBackend tests for instructions on how to run the test suite locally.
4.4.2. Fixing the "application/json" type not supported for POST
When the client tries to create a Todo with a POST of plain JSON, it'll fail with:
POST http://127.0.0.1:8000/api/todos FAILED 415: Unsupported Media Type ({"@context":"\/api\/contexts\/Error","@id":"\/api\/errors\/415","@type":"Error","title":"An error occurred","detail":"The content-type \u0022application\/json\u0022 is not supported. Supported MIME types are \u0022application\/ld+json\u0022.","status":415,"type":"\/errors\/415","trace":[{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/api-platform\/state\/Provider\/ContentNegotiationProvider.php","line":48,"function":"getInputFormat","class":"ApiPlatform\\State\\Provider\\ContentNegotiationProvider","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/api-platform\/symfony\/Controller\/MainController.php","line":83,"function":"provide","class":"ApiPlatform\\State\\Provider\\ContentNegotiationProvider","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/symfony\/http-kernel\/HttpKernel.php","line":181,"function":"__invoke","class":"ApiPlatform\\Symfony\\Controller\\MainController","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/symfony\/http-kernel\/HttpKernel.php","line":76,"function":"handleRaw","class":"Symfony\\Component\\HttpKernel\\HttpKernel","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/symfony\/http-kernel\/Kernel.php","line":197,"function":"handle","class":"Symfony\\Component\\HttpKernel\\HttpKernel","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/symfony\/runtime\/Runner\/Symfony\/HttpKernelRunner.php","line":35,"function":"handle","class":"Symfony\\Component\\HttpKernel\\Kernel","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/vendor\/autoload_runtime.php","line":29,"function":"run","class":"Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner","type":"-\u003E"},{"file":"\/home\/olivier\/git\/gitlab.com\/olberger\/todobackend-symfony6\/public\/index.php","line":5,"function":"require_once"}],"description":"The content-type \u0022application\/json\u0022 is not supported. Supported MIME types are \u0022application\/ld+json\u0022."})
This error 500 displays confirms: Serialization for the format "jsonld" is not supported.
This needs to be fixed by changing config/packages/api_platform.yaml
:
@@ -5,3 +5,10 @@ api_platform: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Origin'] + formats: + # JSON first as it's the default for TodoBackend + json: ['application/json'] + # Then HTML for the swagger API docs + #html: ['text/html'] + jsonld: ['application/ld+json'] +
4.4.3. Adding default value for the 'completed' attribute
We'll change the default values of the completed
and todo_order
attributes for newly created items, to solve
the following error which will happen at runtime:
An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: todo.completed
Here's a proposed change in src/Entity/Todo.php
:
@@ -19,10 +19,10 @@ class Todo private ?string $title = null; #[ORM\Column] - private ?bool $completed = null; + private ?bool $completed = false; #[ORM\Column] - private ?int $todo_order = null; + private ?int $todo_order = 0; public function getId(): ?int {
4.4.4. Adding DELETE on collection
The test suite will attempt to delete all items, which will report the issue: No route found for "DELETE http://127.0.0.1:8000/api/todos": Method Not Allowed (Allow: GET, POST)
The DELETE
on the collection is added with a custom operation
handler TodoDelete
, which uses a method of the TodoRepository
:
We'll add a custom operation handler for DELETE on collections, following advice at https://api-platform.com/docs/core/operations#recommended-method :
Add a new
deleteAll()
method to the Todo Repository class which deletes all entries:class TodoRepository extends ServiceEntityRepository { // ... /** * Delete all instances of Todo * * @return mixed|\Doctrine\DBAL\Driver\Statement|array|NULL */ public function deleteAll() { $isDeleted = $this->createQueryBuilder("todo") ->delete() ->getQuery()->execute(); return $isDeleted; } }
Add a custom Controller as a handler for DELETE operations on collections:
<?php // src/Controller/TodoDeleteCollection.php namespace App\Controller; use App\Repository\TodoRepository; use App\Entity\Todo; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; /** * Custom operation handler for DELETE on collections * see https://api-platform.com/docs/symfony/controllers/ */ class TodoDeleteCollection extends AbstractController { /** * @var TodoRepository used for deleting all items */ private $entityRepository; public function __construct(TodoRepository $entityRepository) { $this->entityRepository = $entityRepository; } public function __invoke(): Response { $this->entityRepository->deleteAll(); return new Response(); } }
Finally, add the custom DELETE configuration for the
ApiResource
annotation of api-platform :// src/Entity/Todo.php //... use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Controller\TodoDeleteCollection; use ApiPlatform\OpenApi\Model\Operation; /** * Todo items * * We add a custom DELETE operation (see https://api-platform.com/docs/core/operations#creating-custom-operations-and-controllers) * */ #[ORM\Entity(repositoryClass: TodoRepository::class)] #[ApiResource( operations: [ new Get(), new Delete(), new HttpOperation(method: HttpOperation::METHOD_DELETE, uriTemplate: '/todos', controller: TodoDeleteCollection::class, openapi: new Operation(summary: 'Removes all Todo resources.')), new GetCollection(), new Post(), new Patch(), ])] class Todo { // ...
Note that you need to explicitely declare the default methods GET and POST, etc. that are necessary too, in addition to the new DELETE method, as in the example above.
4.4.5. Changing default content-type produced to JSON
The default behaviour of the api-platform API, when the collection index is requested, is to respond with JSON-LD, like :
{ "@context": "/api/contexts/Todo", "@id": "/api/todos", "@type": "hydra:Collection", "hydra:member": [], "hydra:totalItems": 0 }
This leads to a problem with the API test suite, reporting something like:
after a DELETE the api root responds to a GET with a JSON representation of an empty array AssertionError: expected { Object (@context, @id, ...) } to deeply equal []
We'll then change the default content-type, by modifying the formats
like the following in config/packages/api_platform.yaml
so that
JSON is the default:
api_platform: ... formats: json: ['application/json'] html: ['text/html'] jsonld: ['application/ld+json']
4.4.6. Adding an url property for the Todo items
In the specs of the TodoBackend API, the JSON representation of Todo
items should include a url
attribute, which point to the URI of the
resource, which is computed by the Router's URL generation method.
We'll then complement the normalization of the items to JSON by adding
a custom ApiNormalizer
in src/Serializer/ApiNormalizer
, following
the guidelines of
https://api-platform.com/docs/core/content-negotiation#writing-a-custom-normalizer.
Note that we don't apply instructions at https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-add-extra-data since we care for JSON and not JSON-LD here.
We do so by modifying config/services.yaml
to add :
services: ... 'App\Serializer\CustomApiNormalizer': arguments: - '@api_platform.serializer.normalizer.item' - '@router'
This injects the Router argument that will be needed to generate the route's URL.
And we'readding the corresponding class in
src/Serializer/CustomApiNormalizer.php
<?php // src/Serializer/CustomApiNormalizer namespace App\Serializer; use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; /** * Add url property to the Todo items * * see https://api-platform.com/docs/core/content-negotiation#writing-a-custom-normalizer * * Note that we don't apply instructions at https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-add-extra-data * since we care for JSON and not JSON-LD here */ final class CustomApiNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface { private $normalizer; /** * @var Router injected to generate the URL from the path */ private $router; public function __construct(NormalizerInterface $normalizer, Router $router) { if (! $normalizer instanceof DenormalizerInterface) { throw new \InvalidArgumentException(sprintf('The normalizer must implement the %s.', DenormalizerInterface::class)); } $this->normalizer = $normalizer; $this->router = $router; } public function supportsNormalization($data, $format = null) { return $this->normalizer->supportsNormalization($data, $format); } public function normalize($object, $format = null, array $context = []) { $data = $this->normalizer->normalize($object, $format, $context); if (is_array($data)) { $url = $this->router->generate('_api_/todos/{id}{._format}_get', [ 'id' => $object->getId() ], UrlGeneratorInterface::ABSOLUTE_URL); $data['url'] = $url; } return $data; } public function supportsDenormalization($data, $type, $format = null) { return $this->normalizer->supportsDenormalization($data, $type, $format); } public function denormalize($data, $class, $format = null, array $context = []) { return $this->normalizer->denormalize($data, $class, $format, $context); } public function setSerializer(SerializerInterface $serializer) { if($this->normalizer instanceof SerializerAwareInterface) { $this->normalizer->setSerializer($serializer); } } }
The normalize()
method accesses the router to generate the URL
corresponding to the GET on todo items (_api_/todos/{id}{._format}_get
)
generated by api-platform (bin/console debug:route
), storing it in
the url
attribute.
4.4.7. Supporting PATCH request on Todo items
The PATCH method should be allowed on items, so we change the
ApiResource
to declare a Patch
item operation, as specifed above.
4.4.8. Fixing the order naming
Final step is to fix the naming of the order
attribute, instead of todo_order
.
Well just rename the getTodoOrder()
and setTodoOrder()
methods of
the Todo
class to (resp.) getOrder()
and setOrder()
.
That's all about it. You should be able to run http://www.todobackend.com/client/index.html?http://127.0.0.1:8000/api/todos now
5. Deploying
5.1. Deploying on heroku
See "Deploying on heroku" in https://gitlab.com/olberger/todobackend-symfony4/-/blob/master/README.org, which may provide some hints
5.2. Deploy on shared hosting with just FTP access
The app should be working on shared hosting with just FTP access (i. e. without composer available through SSH, for instance).
Sources :
- https://medium.com/@runawaycoin/deploying-symfony-4-application-to-shared-hosting-with-just-ftp-access-e65d2c5e0e3d
- https://symfony.com/doc/6.4/deployment.html
Prepare the static files to be deployed:
The following commands should generate the necessary files to be uploaded:
composer dump-env prod APP_ENV=prod composer install --no-dev --optimize-autoloader APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear composer dump-autoload --optimize --no-dev --classmap-authoritative composer require symfony/apache-pack
- Upload the files using ftp to the Web server, in order to get
files split in corresponding dirs. Usually:
- some sort of
public_html/
dir which will contain contents of thepublic/
sources dir (the.htaccess
, theindex.php
and accompanyingbundles/
resources (needed for the OpenAPI client interface of API-Platform) some private dir which isn't served by the Web server, which will contain the rest of the code and data for the application. For instance here
symfony/todobackend-symfony6/
which will list:.env.local.php bin/ composer.json config/ src/ templates/ var/ (recreated empty) vendor/
- some sort of
- Adjust paths and other bits in order to make the Symfony kernel work:
- Modify
index.php
to change some bits:the path to the Symfony code of the app. For instance, imagine that the app is available at http://www.example.com/todobackend/index.php, with that
index.php
present inpublic_html/todobackend/
on the Web server, then the following change is needed, from:require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
into:
require_once dirname(__DIR__).'/../symfony/todobackend-symfony6/vendor/autoload_runtime.php';
next, for some reason, the environment variables don't seem to be passed to the script correctly in my case, so I changed:
Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
to :
Kernel('prod', (bool) false);
You may set it to :
Kernel('prod', (bool) true);
in case of debugging needs…
- Modify
.env.local.php
to set'CORS_ALLOW_ORIGIN' => '*',
- etc.
- Modify