For years, REST (Representational State Transfer) has been the de facto standard when designing APIs. That's quite understandable, as it's a very straight-forward structure: After sending a request to an endpoint, a JSON (or XML) response is returned to the client.
However, as applications became more complicated a recurrent theme started to emerge - multiple REST calls are required to populate a view. Enter GraphQL. With GraphQL, the sender of the request determines the structure of the response, thus providing more flexibility and efficiency to the frontend.
In this article, I will show you how to develop a GraphQL-powered API for an online book store with Symfony.
Prerequisites
To get the most out of this tutorial, you need the following:
- A basic understanding of GraphQL
- Basic experience with PHP and Symfony
- PHP 8.0
- Composer
- The Symfony CLI
Getting Started
Create a new Symfony application, and switch into the newly generated directory, by running the following commands.
symfony new graphql_demo --version=5.4
cd graphql_demo
To provide integration with GraphQL, the application will use a Symfony bundle named overblog/graphql-bundle. At the time of writing, the overblog/graphql-bundle could not be installed to a Symfony 6 project hence the use of Symfony 5.4
Next, install the project dependencies. The project will use the following:
- Doctrine: The Doctrine ORM help manages the application database.
- DoctrineFixturesBundle: This helps load authors and books into the database.
- Faker: Generates fake data for the application.
- Maker: Simplifies creating controllers, entities, and the like.
- OverblogGraphQLBundle: This bundle provides integration with GraphQL.
- OverblogGraphiQLBundle: This bundle provides an integration of the GraphiQL interface.
To install them, run the two commands below.
composer require overblog/graphql-bundle orm -W
composer require --dev overblog/graphiql-bundle maker orm-fixtures fakerphp/faker
Accept the contrib
recipes installation from Symfony Flex by pressing a
. Then, create a .env.local file from the .env file, which Symfony generated during the creation of the project, by running the command below.
cp .env .env.local
This file is ignored by Git as it matches an existing pattern in .gitignore (which Symfony generated). One of the reasons for this file is the accepted best practice of storing your credentials outside of code to keep them safe.
Next, update the DATABASE_URL
parameter in .env.local so that the app uses an SQLite database instead of the PostgreSQL default. To do that, comment out the existing DATABASE_URL
entry and uncomment the SQLite option, so that it matches the example below.
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
The database will be created in ./var and be named data.db.
Create entities
The application will have two entities, Author
and Book
. Create the Author
entity first using the following command.
symfony console make:entity Author
The CLI will ask some questions and add fields for the entity based on the provided answers. Answer the questions as shown below.
New property name (press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Author.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> dateOfBirth
Field type (enter ? to see all types) [string]:
> datetime
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Author.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> bio
Field type (enter ? to see all types) [string]:
> text
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Author.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Press the "Enter" key to complete the creation process for the Author
entity.
Next, create the Book
entity using the following command.
symfony console make:entity Book
Respond to the CLI prompt as shown below.
New property name (press <return> to stop adding fields):
> title
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
Add another property? Enter the property name (or press <return> to stop adding fields):
> author
Field type (enter ? to see all types) [string]:
> ManyToOne
What class should this entity be related to?:
> Author
Is the Book.author property allowed to be null (nullable)? (yes/no) [yes]:
> no
Do you want to add a new property to Author so that you can access/update Book objects from it - e.g. $author->getBooks()? (yes/no) [yes]:
> yes
A new property will also be added to the Author class so that you can access the related Book objects from it.
New field name inside Author [books]:
> books
Do you want to activate orphanRemoval on your relationship?
A Book is "orphaned" when it is removed from its related Author.
e.g. $author->removeBook($book)
NOTE: If a Book may *change* from one Author to another, answer "no".
Do you want to automatically delete orphaned App\\Entity\\Book objects (orphanRemoval)? (yes/no) [no]:
> yes
updated: src/Entity/Book.php
updated: src/Entity/Author.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> synopsis
Field type (enter ? to see all types) [string]:
> text
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Book.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> releaseYear
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 4
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Book.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> genre
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
Add another property? Enter the property name (or press <return> to stop adding fields):
> averageRating
Field type (enter ? to see all types) [string]:
> integer
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Book.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> copiesSold
Field type (enter ? to see all types) [string]:
> integer
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Book.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Press the "Enter" key to complete the creation process for the Book entity.
Update the entities
Open the Author
entity in the src/Entity/Author.php file and update its content to match the following.
<?php
namespace App\Entity;
use App\Repository\AuthorRepository;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AuthorRepository::class)]
class Author
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'datetime')]
private DateTimeInterface $dateOfBirth;
#[ORM\Column(type: 'text')]
private string $bio;
#[ORM\OneToMany(
mappedBy: 'author',
targetEntity: Book::class,
orphanRemoval: true
)]
private Collection $books;
public function __construct(
string $name,
DateTimeInterface $dateOfBirth,
string $bio
) {
$this->name = $name;
$this->dateOfBirth = $dateOfBirth;
$this->bio = $bio;
$this->books = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getDateOfBirth(): string
{
return $this->dateOfBirth->format('l F jS, Y');
}
public function setDateOfBirth(DateTimeInterface $dateOfBirth): void
{
$this->dateOfBirth = $dateOfBirth;
}
public function getBio(): ?string
{
return $this->bio;
}
public function setBio(string $bio): void
{
$this->bio = $bio;
}
public function getBooks(): Collection
{
return $this->books;
}
public function addBook(Book $book): void
{
if (!$this->books->contains($book)) {
$this->books[] = $book;
$book->setAuthor($this);
}
}
public function removeBook(Book $book): void
{
if ($this->books->removeElement($book)) {
// set the owning side to null (unless already changed)
if ($book->getAuthor() === $this) {
$book->setAuthor(null);
}
}
}
}
In addition to adding types for the fields of the Author
entity, the revised code adds a constructor function that requires the author’s name, date of birth, and bio. It also modifies the getDateOfBirth()
function to return a string corresponding to the date of birth, but in a human-readable format. Finally, it modifies the setter functions so that they return nothing, as they will not be used.
Lastly, open src/Entity/Book.php and update its content to match the following.
<?php
namespace App\Entity;
use App\Repository\BookRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BookRepository::class)]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
#[ORM\Column(type: 'string', length: 255)]
private string $title;
#[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')]
#[ORM\JoinColumn(nullable: false)]
private Author $author;
#[ORM\Column(type: 'text')]
private string $synopsis;
#[ORM\Column(type: 'string', length: 4)]
private string $releaseYear;
#[ORM\Column(type: 'string')]
private string $genre;
#[ORM\Column(type: 'integer')]
private int $averageRating;
#[ORM\Column(type: 'integer')]
private int $copiesSold;
public function __construct(
string $title,
Author $author,
string $synopsis,
string $releaseYear,
int $averageRating,
int $copiesSold,
string $genre
) {
$this->title = $title;
$this->author = $author;
$this->synopsis = $synopsis;
$this->releaseYear = $releaseYear;
$this->averageRating = $averageRating;
$this->copiesSold = $copiesSold;
$this->genre = $genre;
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getAuthor(): ?Author
{
return $this->author;
}
public function setAuthor(?Author $author): void
{
$this->author = $author;
}
public function getSynopsis(): ?string
{
return $this->synopsis;
}
public function setSynopsis(string $synopsis): void
{
$this->synopsis = $synopsis;
}
public function getReleaseYear(): ?string
{
return $this->releaseYear;
}
public function setReleaseYear(string $releaseYear): void
{
$this->releaseYear = $releaseYear;
}
public function getAverageRating(): ?int
{
return $this->averageRating;
}
public function setAverageRating(int $averageRating): void
{
$this->averageRating = $averageRating;
}
public function getCopiesSold(): ?int
{
return $this->copiesSold;
}
public function setCopiesSold(int $copiesSold): void
{
$this->copiesSold = $copiesSold;
}
public function getGenre(): string
{
return $this->genre;
}
public function setGenre(string $genre): void
{
$this->genre = $genre;
}
public function __set(string $property, mixed $value): void
{
if (property_exists($this, $property)) {
$this->$property = $value;
}
}
}
The modifications to the Book
entity are similar to the ones made for the Author
entity. Types for the fields were added, a constructor function was declared, and the setter functions were modified to have void
as their return type. In addition, a magic setter for the Book
entity was also added, to support updating multiple fields in one request.
Next, update the database schema by running the following command.
symfony console doctrine:schema:update --force
Create fixtures
To give us interesting data when we’re developing the API, let’s create some fixtures which will load fake data into the database. Start by creating a fixture to load authors by running the following command.
symfony console make:fixture AuthorFixtures
Then, open the newly created src/DataFixtures/AuthorFixtures.php file and update its content to match the following.
<?php
namespace App\DataFixtures;
use App\Entity\Author;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;
use Faker\Generator;
class AuthorFixtures extends Fixture
{
public const REFERENCE = 'AUTHORS_REFERENCE';
private Generator $faker;
public function __construct()
{
$this->faker = Factory::create();
}
public function load(ObjectManager $manager): void
{
for ($i = 0; $i < 10; $i++) {
$manager->persist(
$this->getFakeAuthor()
);
}
$referenceAuthor = $this->getFakeAuthor();
$this->addReference(self::REFERENCE, $referenceAuthor);
$manager->persist($referenceAuthor);
$manager->flush();
}
private function getFakeAuthor(): Author
{
return new Author(
$this->faker->name(),
$this->faker->dateTime(),
$this->faker->sentences(4, true)
);
}
}
The fixture adds 10 authors to the database using details generated by Faker. Before saving the changes to the database the addReference()
function is called, passing to it the REFERENCE
constant and an author. This provides access to the author in the fixture for loading books.
Create a new fixture for the Book
entity using the following command.
symfony console make:fixture BookFixtures
Open the newly created src/DataFixtures/BookFixtures.php file and update its content to match the following.
<?php
namespace App\DataFixtures;
use App\Entity\Book;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;
use Faker\Generator;
class BookFixtures extends Fixture implements DependentFixtureInterface
{
private Generator $faker;
public function __construct()
{
$this->faker = Factory::create();
}
public function load(ObjectManager $manager): void
{
for ($i = 0; $i < 100; $i++) {
$manager->persist(
$this->getFakeBook()
);
}
$manager->flush();
}
private function getFakeBook(): Book
{
$genres = ['Action', 'Comedy', 'Romance', 'Sci-fi', 'Programming'];
return new Book(
$this->faker->sentence(),
$this->getReference(AuthorFixtures::REFERENCE),
$this->faker->sentences(5, true),
$this->faker->year(),
$this->faker->numberBetween(1, 10),
$this->faker->numberBetween(10000, 10000000),
$this->faker->randomElement($genres)
);
}
public function getDependencies(): array
{
return [
AuthorFixtures::class,
];
}
}
The first thing to note is that this fixture implements what is called a DependentFixtureInterface. Because a book requires an author in its constructor, the AuthorsFixture
must run before the BooksFixture
. This dependency is specified in the getDependencies()
function. By doing so, the fixtures run in the expected order.
The books are created using details from Faker. But notice that for the author parameter, the getReference()
function is called. This retrieves the reference author in the AuthorFixture
.
Run the fixtures using the following command.
symfony console doctrine:fixtures:load -n
Once the command finishes running, confirm that the database contains authors by running the following command.
symfony console doctrine:query:sql "select * from author;"
Implement GraphQL functionality
Define the schema
The next thing we will do is define the schema for our API. In the config/graphql/types folder, you’ll create a schema for the author and book entities. In addition, you’ll declare schemas for the mutations and queries.
The API will handle the following queries:
- Query for an author based on id
- Query for all authors
- Query for an author via the author’s name
- Query for all books
- Query for books based on genre
- Query for books based on id
For mutations, it will have the following mutations:
- Create a new author
- Update the details of a book
So, start with the Author
entity. In the config/graphql/types folder, create a new file called Author.graphql and add the following to it.
type Author {
id: ID!
name: String!
dateOfBirth: String!
bio: String!
books: [Book!]!
}
input CreateAuthor {
name: String!
dateOfBirth: String!
bio: String!
}
Next, create another file in the config/graphql/types folder named Book.graphql and add the following to it.
type Book {
id: ID!
title: String!
author: Author!
synopsis: String!
releaseYear: String!
genre: String!
averageRating: Int!
copiesSold: Int!
}
input UpdateBook {
title: String
author: ID
synopsis: String
releaseYear: String
genre: String
averageRating: Int
copiesSold: Int
}
With the types and inputs for the Author
and Book
entities in place, define the schemas for the queries and mutations. To do this, in the config/graphql/types folder, create a new file called query.graphql and add the following code to it.
type RootQuery {
author(id: ID!): Author
authors: [Author!]!
findBooksByAuthor(name: String!) : [Book!]!
books: [Book!]!
findBooksByGenre(genre: String!) : [Book!]!
book(id:ID!): Book
}
This defines the permitted queries for the API, specifying the parameters (where required) and expected types. It also declares the return types for each query.
Next, create another file in the config/graphql/types folder named mutations.graphql and add the following to it.
type RootMutation {
createAuthor(author: CreateAuthor!): Author!
updateBook(id: ID!, book: UpdateBook!): Book!
}
Similar to the query schema, it specifies the required parameters for each mutation. It also use the inputs declared for creating an author as well as updating a book in the Author.graphql and Book.graphql schemas respectively.
Create the services
To separate concerns, next create services to handle the query and mutation requests. In the src folder at the root of the project, create a new folder called Service.
Create a query service
In the src/Service folder, create a new file called QueryService.php and add the following to it.
<?php
namespace App\Service;
use App\Entity\Author;
use App\Entity\Book;
use App\Repository\AuthorRepository;
use App\Repository\BookRepository;
use Doctrine\Common\Collections\Collection;
class QueryService
{
public function __construct(
private AuthorRepository $authorRepository,
private BookRepository $bookRepository,
) {}
public function findAuthor(int $authorId): ?Author
{
return $this->authorRepository->find($authorId);
}
public function getAllAuthors(): array
{
return $this->authorRepository->findAll();
}
public function findBooksByAuthor(string $authorName): Collection
{
return $this
->authorRepository
->findOneBy(['name' => $authorName])
->getBooks();
}
public function findAllBooks(): array
{
return $this->bookRepository->findAll();
}
public function findBooksByGenre(string $genre): array
{
return $this->bookRepository->findBy(['genre' => $genre]);
}
public function findBookById(int $bookId): ?Book
{
return $this->bookRepository->find($bookId);
}
}
In this service, we declare a function to handle all the requests specified in config/graphql/types/query.graphql. Using the AuthorRepository
and BookRepository
which were injected as constructor dependencies, we are able to retrieve books and authors based on the parameters provided in the request.
Create a mutation service
In the src/Service folder, create a new file called MutationService.php and add the following to it.
<?php
namespace App\Service;
use App\Entity\Author;
use App\Entity\Book;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use GraphQL\Error\Error;
class MutationService
{
public function __construct(
private EntityManagerInterface $manager
) {}
public function createAuthor(array $authorDetails): Author
{
$author = new Author(
$authorDetails['name'],
DateTime::createFromFormat('d/m/Y', $authorDetails['dateOfBirth']),
$authorDetails['bio']
);
$this->manager->persist($author);
$this->manager->flush();
return $author;
}
public function updateBook(int $bookId, array $newDetails): Book
{
$book = $this->manager->getRepository(Book::class)->find($bookId);
if (is_null($book)) {
throw new Error("Could not find book for specified ID");
}
foreach ($newDetails as $property => $value) {
$book->$property = $value;
}
$this->manager->persist($book);
$this->manager->flush();
return $book;
}
}
The service receives an EntityManagerInterface
for persisting and flushing changes to the database.
In the createAuthor()
function, we create a new author from the information provided in the authorDetails
array. One key thing to note is the date format which the API is expecting for the author’s date of birth (dd/mm/yyyy
).
In the updateBook()
function, we retrieve the book based on the provided id. If the book doesn’t exist, we throw an Error
with an appropriate message. If we find a book with the specified id, we then update the properties based on the information provided in the update request. Finally, we save the changes and return the updated book.
The QueryService
and MutationService
will be used by the ResolverMap
which will be created next.
Create a resolver
Our API now has a schema, but we still need a way to resolve the queries/mutations and provide the required response. We do this via a ResolverMap. In the src folder at the root of the project, create a new folder called Resolver. In it, create a new file called CustomResolverMap.php and add the following code to it.
<?php
namespace App\GraphQL\Resolver;
use App\Service\MutationService;
use App\Service\QueryService;
use ArrayObject;
use GraphQL\Type\Definition\ResolveInfo;
use Overblog\GraphQLBundle\Definition\ArgumentInterface;
use Overblog\GraphQLBundle\Resolver\ResolverMap;
class CustomResolverMap extends ResolverMap
{
public function __construct(
private QueryService $queryService,
private MutationService $mutationService
) {}
/**
* @inheritDoc
*/
protected function map(): array
{
return [
'RootQuery' => [
self::RESOLVE_FIELD => function (
$value,
ArgumentInterface $args,
ArrayObject $context,
ResolveInfo $info
) {
return match ($info->fieldName) {
'author' => $this->queryService->findAuthor((int)$args['id']),
'authors' => $this->queryService->getAllAuthors(),
'findBooksByAuthor' => $this->queryService->findBooksByAuthor($args['name']),
'books' => $this->queryService->findAllBooks(),
'findBooksByGenre' => $this->queryService->findBooksByGenre($args['genre']),
'book' => $this->queryService->findBookById((int)$args['id']),
default => null
};
},
],
'RootMutation' => [
self::RESOLVE_FIELD => function (
$value,
ArgumentInterface $args,
ArrayObject $context,
ResolveInfo $info
) {
return match ($info->fieldName) {
'createAuthor' => $this->mutationService->createAuthor($args['author']),
'updateBook' => $this->mutationService->updateBook((int)$args['id'], $args['book']),
default => null
};
},
],
];
}
}
Here, we inject the QueryService
and MutationService
as constructor dependencies. Next, we override the map()
function which expects an array as the return type. At the root of the array, we declare the resolvers for our RootQuery
and RootMutation
.
Using the information passed in the ResolveInfo
object, we match the fieldName
provided against the expected names which were declared in our query and mutation schemas. Based on the value of fieldName
, we return the response from the associated service call, passing the provided arguments where required. As a default, we return null
for unknown field names.
With our resolver in place, we can add our service configuration and run our API.
Configure the Overblog bundle
Open config/packages/graphql.yaml and update its content to match the following.
overblog_graphql:
definitions:
schema:
mutation: RootMutation
query: RootQuery
mappings:
types:
-
type: graphql
dir: "%kernel.project_dir%/config/graphql/types"
suffix: null
Here, we change the mapping type from YAML to GraphQL which is the format used in declaring the API schema in config/graphql/types.
Next, we need to tag the resolver with the overblog_graphql.resolver_map
tag. To do this, open config/services.yaml and update it to match the following.
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
App\GraphQL\Resolver\CustomResolverMap:
tags:
- { name: overblog_graphql.resolver_map, schema: default }
With this in place, we can run our API using the following command.
symfony serve
By default, the application will run on port 8000. To access the GraphiQL interface and test your database, navigate to https://localhost:8000/graphiql.
Test the endpoints
Return all books
Retrieve a list of books by running the following query in the query window:
query getAllBooks {
books {
id,
title,
genre
}
}
This returns the collection of books seeded in the database.
Find all authors
Run the following query to retrieve the list of authors
query authors {
authors {
id,
name,
dateOfBirth,
books {
title,
averageRating
}
}
}
This will return the id, name, dateOfBirth and books for the authors, as shown here:
Find an author with an id
Next, use the query below to fetch the details of a particular author using the author's id as a unique identifier:
query authors {
author(id: 11) {
name,
dateOfBirth,
bio,
books {
title
}
}
}
You will see the same query as shown in the image below:
Find books with a specific author
Next, to retrieve the list of books for a specific author using the author's name as the identifier, by using the following query:
{
findBooksByAuthor(name:"Mr. Emmet Brekke") {
title,
genre
}
}
Create an author
Here, we will start with mutations by using the following query to create a new author:
mutation CreateAuthor {
createAuthor(author: {name: "Sample Author", dateOfBirth: "09/12/1990" , bio: "Great Author"}){
name,
dateOfBirth
}
}
That's how to develop a GraphQL-Powered API with Symfony
In this article, you built a GraphQL powered API using Symfony. Using GraphQL, the client is able to specify the structure of the response (based on a previously agreed schema) thereby dealing with the issue of over or under-fetching. Additionally, we are able to leverage the type-checking provided out of the box to prevent unwanted bugs.
The code for this article is available here on GitHub. Happy coding!
Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.
A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.
- Twitter: https://twitter.com/yemiwebby
- GitHub: https://github.com/yemiwebby
- Website: https://yemiwebby.com.ng/

In the article, you will learn how to create a drag-and-drop file upload system in CakePHP using Dropzone.js, which leverages AJAX to upload files without requiring a page refresh.

This tutorial will teach you the essentials of implementing CRUD operations in CakePHP. It illustrates how users can create, read, update, and delete records, thus providing a guide to managing data in your application in CakePHP.

In this tutorial, you will learn how to export data from a MySQL database to a CSV file with the CakePHP framework.

In this tutorial, you will get an in-depth exploration of Docker — in the context of Laravel. Then, rather than relying on Laravel Sail's pre-configured environment, you will learn how to run Laravel inside a Docker and deploy it with Docker Compose.

In this tutorial, you will learn how you can use the Mercure protocol in your Symfony applications to broadcast updates to the frontend.

In this tutorial, you will learn how to upload files in CakePHP