Back-end17 minute read

PHP Frameworks: Symfony vs. Laravel

Many popular languages for web development have their ‘default’ framework, such as Ruby on Rails for Ruby, or Django for Python. However, PHP has no such single default and has multiple popular options to choose from.

In this article, Toptal Freelance Developer Karim Sakhibgareev compares the two most popular PHP frameworks, Symfony and Laravel, explores their features, and establishes guidelines for how PHP developers should choose between them.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Many popular languages for web development have their ‘default’ framework, such as Ruby on Rails for Ruby, or Django for Python. However, PHP has no such single default and has multiple popular options to choose from.

In this article, Toptal Freelance Developer Karim Sakhibgareev compares the two most popular PHP frameworks, Symfony and Laravel, explores their features, and establishes guidelines for how PHP developers should choose between them.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Karim Sakhibgareev
Verified Expert in Engineering
20 Years of Experience

Having worked with Laravel for Media.net and with Symfony for Into Film, Karim is acquainted with the pros and cons of both frameworks.

Read More

Expertise

Share

Today, when starting a new project, one of the key decisions is to pick the right framework. It’s become hard to imagine building a complex web application from scratch nowadays without one.

Many popular languages for web development have their “default” framework, such as Ruby on Rails for Ruby, or Django for Python. However, PHP has no such single default and has multiple popular options to choose from.

According to Google trends and GitHub, the most popular PHP frameworks are Symfony with 13.7k stars and Laravel with 29k stars (at the time of writing this article).

In this article, I am going to compare these two frameworks and show you how to implement simple, everyday features with each. This way, you can compare the code of real-life examples side by side.

This article presumes strong PHP skills and an understanding of the MVC architectural paradigm, but no previous experience with Symfony or Laravel is required.

Laravel vs. Symfony: The Contenders

Laravel

When speaking about Laravel, we are referring to Laravel version 4 and beyond. Laravel 4 was released in 2013 and represented a complete rewrite of the framework. The functionality of the framework was decoupled into separate components, which were managed with Composer, instead of everything being in one single huge code repository.

Laravel declares itself as a framework for rapid development with a simple and beautiful syntax which is easy to learn, read, and maintain. It is the most popular framework in 2016. According to Google trends, it is three times more popular than other frameworks, and on GitHub, it has two times more stars than competitors.

Symfony

Symfony 2 was released in 2011, but it must not be confused with Symfony 1, which was a totally different framework with different underlying principles. Fabien Potencier created Symfony 2, and the current version is 3.2, which is an incremental version of Symfony 2. Therefore, they are often called simply Symfony2/3.

Like Laravel 4, Symfony 2 is designed as a set of decoupled components. There are two benefits here: We can replace any component in a Symfony project, and we can take and use any Symfony component in a non-Symfony project. Symfony components can serve as great code examples and they are used in a lot of open source projects such as Drupal, phpBB, and Codeception. In fact, Laravel itself uses no less than 14 Symfony components. Understanding the Symfony framework thus gives you many benefits when working with other projects.

Framework Installations

Both frameworks come with installers and wrappers available via the PHP built-in web server.

Symfony Installation

Symfony installation is as simple as the following:

# Downloading Symfony installer
sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
# Granting permissions to execute installer
sudo chmod a+x /usr/local/bin/symfony
# Creating new Symfony project
symfony new symfony_project
# Launching built-in server
cd symfony_project/ && php bin/console server:start

That’s it! Your Symfony installation is available on URL http://localhost:8000.

Laravel Installation

The Laravel installation process is almost the same and as simple as that for Symfony; the only difference between Laravel and Symfony installation is that you obtain Laravel’s installer through Composer:

# Downloading Laravel installer using Composer
composer global require "laravel/installer"
# Creating new Laravel project
laravel new laravel_project
# Launching built-in server
cd laravel_project/ && php artisan serve

You can now visit http://localhost:8000 and check your Laravel installation.

Note: Both Laravel and Symfony run off the same localhost port (8000) by default, so you can’t have these default instances running concurrently. Don’t forget to stop the Symfony server by running php bin/console server:stop before launching the Laravel server.

About Framework Installation

These are examples of a basic installation. For more advanced usage examples, such as being able to configure projects with local domains or run multiple projects at once, both frameworks provide Vagrant boxes:

Basic Framework Configurations

Symfony Basic Configuration

Symfony uses YAML as the syntax for specifying its configuration. The default configuration is located in the app/config/config.yml file, and looks like the following example:

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

framework:
    secret:          '%secret%'
    router:          { resource: '%kernel.root_dir%/config/routing.yml' }
    # ...

# Twig Configuration
twig:
    debug:            '%kernel.debug%'
    strict_variables: '%kernel.debug%'
    
# ...

To create an environment specific configuration, create the file app/config/config_ENV.yml containing the basic config parameters. Here’s an example of a config_dev.yml file for the development environment:

imports:
    - { resource: config.yml }
# ...
web_profiler:
    toolbar: true
# ...

This example turns on the web_profiler Symfony tool only for the development environment. This tool helps you to debug and profile your application right in the browser window.

In the configuration files, you can also notice %secret% constructions. They allow us to put environment-specific variables in the separate parameters.yml file. This file could be unique on every machine and is not stored under version control. For version control, we have a special parameters.yml.dist file that is the template for the parameters.yml file.

Here is an example of the parameters.yml file:

parameters:
    database_host: 127.0.0.1
    database_port: null
    database_name: symfony
    database_user: root
    database_password: null
    secret: f6b16aea89dc8e4bec811dea7c22d9f0e55593af

Laravel Basic Configuration

Laravel configuration looks very different from that of Symfony. The only one thing they have in common is that they both use files that are not stored under version control (.env in the Laravel case) and a template for generating this file (.env.example). This file has a list of keys and values, like the following example:

APP_ENV=local
APP_KEY=base64:Qm8mIaur5AygPDoOrU+IKecMLWgmcfOjKJItb7Im3Jk=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost

Like the Symfony YAML file, this one for Laravel is also human readable and looks clean. You can additionally create .env.testing file that will be used when running PHPUnit tests.

The application configuration is stored in .php files in the config directory. Basic configuration is stored in the app.php file and component-specific configuration is stored in <component>.php files (e.g., cache.php or mail.php). Here’s an example of a config/app.php file:

<?php

return [
    'name'     => 'Laravel',
    'env'      => env('APP_ENV', 'production'),
    'debug'    => env('APP_DEBUG', false),
    'url'      => env('APP_URL', 'http://localhost'),
    'timezone' => 'UTC',
    'locale'   => 'en',
    // ...
];

Framework Configuration: Symfony vs. Laravel

Symfony’s application configuration mechanisms allow you to create different files for different environments. Additionally, it prevents you from injecting complex PHP logic in the YAML configuration.

However, you may feel more comfortable with the default PHP configuration syntax Laravel is using and you don’t have to learn YAML syntax.

Routing and Controller

In general, a back-end web application has one primary responsibility: to read each request and create a response depending on the content of the request. The controller is a class responsible for transforming the request to the response by calling application methods, while the router is a mechanism that helps you to detect which controller class and method you should execute for a particular request.

Let’s create a controller that will show a blog post page requested from the /posts/{id} route.

Routing and Controller in Laravel

Controller

<?php

namespace App\Http\Controllers;

use App\Post;

class BlogController extends Controller
{
    /**
     * Show the blog post
     * @param int $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        return view('post', ['post' => Post::findOrFail($id)]);
    }
}

Router

Route::get('/posts/{id}', 'BlogController@show');

We’ve defined the route for GET requests. All requests with the URI matching the /posts/{id} will execute the BlogController controller’s show the method, and will pass the parameter id to that method. In the controller, we are trying to find the object of model POST with the passed id, and call Laravel helper view() to render the page.

Routing and Controller in Symfony

In Symfony, exampleController is a little bit bigger:

<?php

namespace BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class PostController extends Controller
{
    /**
     * @Route("/posts/{id}")
     * @param int $id
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function indexAction($id)
    {
        $repository = $this->getDoctrine()->getRepository('BlogBundle:Post');
        $post = $repository->find($id);
        if ($post === null) {
            throw $this->createNotFoundException();
        }
        return $this->render('BlogBundle:Post:show.html.twig', ['post'=>$post]);
    }
}

You can see we’ve already included @Route("/posts/{id}”) in the annotation, so we just need to include the controller in the routing.yml configuration file:

blog:
    resource: "@BlogBundle/Controller/"
    type:     annotation
    prefix:   /

The step by step logic is the same as in the Laravel case.

Routing and Controller: Symfony vs. Laravel

At this stage, you might think Laravel is much nicer than Symfony. This is true, in the beginning. It looks way better and easier to start. However, in the real-life applications, you shouldn’t call Doctrine from the controller. Instead, you should call a service that will try to find the post or throw HTTP 404 Exception.

Templates

Laravel ships with a template engine called Blade and Symfony ships with Twig. Both template engines implement two main features:

  1. Template inheritance
  2. Blocks or sections

Both features allow you to define a basic template with overridable sections and child templates that fill values of those sections.

Let’s consider the example of a blog post page again.

Laravel Blade Template Engine

// base.blade.php
<html>
<head>
    <style></style>
    <title>@section('page-title')
            Welcome to blog!
        @show
    </title>
</head>
<body>
<div class="container">
    <h1>@yield('title')</h1>
    <div class="row">
        @yield('content')
    </div>
</div>
</body>
</html>

// post.blade.php
@extends('base')

@section('page-title')Post {{ $post->title }} - read this and more in our blog.@endsection

@section('title'){{ $post->title }}@endsection

@section('content')
    {{ $post->content }}
@endsection

Now you can tell Laravel in your Controller to render template post.blade.php. Do you remember our view(‘post’, …) call in the previous Controller example? You don’t need to know in your code that it is inherited from some other template. It is all defined only in your templates, on the view level.

Symfony Twig Template Engine

// base.html.twig
<html>
<head>
    <style></style>
    <title>{% block page_title %}
        Welcome to blog!
        {% endblock %}
    </title>
</head>
<body>
<div class="container">
    <h1>{% block title %}{% endblock %}</h1>
    <div class="row">
        {% block content %}{% endblock %}
    </div>
</div>
</body>
</html>

// show.html.twig
{% extends '@Blog/base.html.twig' %}

{% block page_title %}Post {{ post.title }} - read this and more in our blog.{% endblock %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
    {{ post.content }}
{% endblock %}

Templates: Symfony vs. Laravel

Structurally, Blade and Twig templates are quite similar. Both generate templates into PHP code and work fast, and both implement control structures, such as if statements and loops. The most important feature both engines have is that they escape the output by default, which helps prevents XSS attacks.

Aside from syntax, the main difference is that Blade allows you to inject PHP code directly into your templates and Twig does not. Instead, Twig allows you to use filters.

For example, if you want to capitalize a string, in Blade you would specify the following:

{{ ucfirst('welcome friend') }}

In Twig, on the other hand, you would do the following:

{{ 'welcome friend'|capitalize }}

In Blade, it is easier to extend some functionality, but Twig does not allow any direct PHP code in templates.

Dependency Injection

Applications have a lot of different services and components, with various interdependencies. You need to store all the information about the created objects and their dependencies somehow.

Here comes our next component - Service Container. It is a PHP object that creates requested services and stores information about the created objects and their dependencies.

Let’s consider the following example: You are creating a class PostService to implement a method that is responsible for creating a new blog post. This class depends on two other services: PostRepository, which is responsible for storing information in the database, and SubscriberNotifier, which is responsible for notifying subscribed users about the new post. To get it to work, you need to pass these two services as the constructor arguments of PostService or, in other words, you need to inject these dependencies.

Symfony Dependency Injection Example

First, let’s define our sample services:

<?php
// src/BlogBundle/Repository/PostRepository.php
namespace BlogBundle\Repository;

use BlogBundle\Entity\Post;
use Doctrine\ORM\EntityRepository;

class PostRepository extends EntityRepository
{
    public function persist(Post $post)
    {
        // Perform save to db
    }
}
<?php
// src/BlogBundle/Service/SubscriberNotifier.php
namespace BlogBundle\Service;

use BlogBundle\Entity\Post;

class SubscriberNotifier
{
    public function notifyCreate(Post $post)
    {
        // Notify subscribers
    }
}
<?php
// src/BlogBundle/Service/PostService
namespace BlogBundle\Service;

use BlogBundle\Entity\Post;
use BlogBundle\Repository\PostRepository;

class PostService
{
    /** @var PostRepository */
    private $repository;
    /** @var SubscriberNotifier */
    private $notifier;

    function __construct(PostRepository $repository, SubscriberNotifier $notifier)
    {
        $this->repository = $repository;
        $this->notifier = $notifier;
    }

    public function create(Post $post)
    {
        $this->repository->persist($post);
        $this->notifier->notifyCreate($post);
    }
}

Next is the dependency injection configuration:

# src/BlogBundle/Resources/config/services.yml
services:
    # Our main service
    blog.post_service:
      class: BlogBundle\Service\PostService
      arguments: ['@blog.post_repository', '@blog.subscriber_notifier']

    # SubscriberNotifier service. It could also have its own dependencies, for example, mailer class.
    blog.subscriber_notifier:
      class: BlogBundle\Service\SubscriberNotifier

    # Repository. Don't dive deep into it's configuration, it is not a subject now
    blog.post_repository:
      class: BlogBundle\Repository\PostRepository
      factory: 'doctrine.orm.default_entity_manager:getRepository'
      arguments:
        - BlogBundle\Entity\Post

Now you can request your post service anywhere in the code from your Service Container object. For example, in the controller it could be something like this:

// Controller file. $post variable defined below
$this->get('blog.post_service')->create($post);

Service Container is a great component, and it helps to build your application following SOLID design principles.

Laravel Dependency Injection Example

It is much easier to manage dependencies in Laravel. Let’s consider the same example:

<?php
// app/Repository/PostRepository.php
namespace App\Repository;

use App\Post;

class PostRepository
{
    public function persist(Post $post)
    {
        // Perform save to db
    }
}
<?php
// app/Service/SubscriberNotifier.php
namespace App\Service;

use App\Post;

class SubscriberNotifier
{
    public function notifyCreate(Post $post)
    {
        // Notify subscribers
    }
}
<?php
// app/Service/PostService.php
namespace App\Service;

use App\Post;
use App\Repository\PostRepository;

class PostService
{
    /** @var PostRepository */
    private $repository;
    /** @var SubscriberNotifier */
    private $notifier;

    public function __construct(PostRepository $repository, SubscriberNotifier $notifier)
    {
        $this->repository = $repository;
        $this->notifier = $notifier;
    }

    public function create(Post $post)
    {
        $this->repository->persist($post);
        $this->notifier->notifyCreate($post);
    }
}

Here comes the beauty of Laravel - you do not need to create dependency configurations. Laravel automatically scans the dependencies for PostService in its constructor arguments types and automatically resolves them.

You can also use injection in your controller method to use PostService by “type-hinting” it in method arguments:

<?php

namespace App\Http\Controllers;

use App\Post;
use App\Service\PostService;

class BlogController extends Controller
{
    public function create(PostService $service)
    {
        $post = new Post(['title' => 'Title', 'content' => 'Content']);

        $service->create($post);
        return redirect('/posts/'.$post->id);
    }
}

Dependency Injection: Symfony vs. Laravel

Laravel’s autodetection works great. Symfony has a similar capability called “autowire” that is turned off by default and could be turned on by adding autowire: true to your dependency configuration, but it requires some configuration. The Laravel way is simpler.

Object Relational Mapping (ORM)

To work with databases, both frameworks come with Object-Relational Mapping (ORM) features. ORM maps records from the database to objects in the code. To do this, you must create models for each record type (or each table) in your database.

Symfony uses a third-party project Doctrine to interact with the database, while Laravel uses its own library Eloquent.

The Eloquent ORM implements the ActiveRecord pattern to work with the database. In this pattern, each model is aware of the connection to the database and can interact with it. For example, it can save data to the database, update, or delete a record.

Doctrine implements the Data Mapper pattern, where models know nothing about the database; they are only aware of the data itself. A special separate layer, EntityManager, stores all the information about the interaction between models and databases, and it handles all the operations.

Let’s take an example to understand the difference. Let’s say your model has a primary id key, title, content, and author. The Posts table stores only the author id, so you need to create a relation to the Users table.

Doctrine

Let’s begin by defining the models:

<?php
// src/BlogBundle/Entity/User.php
namespace BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity
 */
class User
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;
}
<?php
// src/BlogBundle/Entity/Post.php
namespace BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Post
 *
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="BlogBundle\Repository\PostRepository")
 */
class Post
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     */
    protected $title;

    /**
     * @var string
     *
     * @ORM\Column(name="content", type="text")
     */
    protected $content;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="BlogBundle\Entity\User")
     * @ORM\JoinColumn(name="author_id", referencedColumnName="id")
     */
    protected $author;

Here, we created model mapping information and can now use a helper to generate the method stubs:

php bin/console doctrine:generate:entities BlogBundle

Next, we define post repository methods:

<?php
// src/BlobBundle/Repository/PostRepository.php
namespace BlogBundle\Repository;

use BlogBundle\Entity\Post;
use Doctrine\ORM\EntityRepository;

class PostRepository extends EntityRepository
{
    /**
     * Store post to database
     *
     * @param Post $post
     */
    public function persist(Post $post)
    {
        $this->getEntityManager()->persist($post);
        $this->getEntityManager()->flush();
    }

    /**
     * Search posts with given author's name
     *
     * @param string $name
     * @return array
     */
    public function findByAuthorName($name)
    {
        return $this->createQueryBuilder('posts')
            ->select('posts')
            ->join('posts.author', 'author')
            ->where('author.name = :name')
            ->setParameter('name', $name)
            ->getQuery()
            ->getResult();
    }
}

Now you can call these methods from the service or, for example, from PostController:

// To search for posts
$posts = $this->getDoctrine()->getRepository('BlogBundle:Post')->findByAuthorName('Karim');
// To save new post in database
$this->getDoctrine()->getRepository('BlogBundle:Post')->persist($post);

Eloquent

The User model ships with Laravel and it is defined by default, so you only need to define one model for the Post.

<?php
// app/Post.php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function author()
    {
        return $this->belongsTo('App\User', 'author_id');
    }
}

That’s all for models. In Eloquent, you don’t have to define model properties, as it builds them dynamically based on the database table structure. To store a new post $post into the database, you need to make this call (from the controller, for example):

$post->save();

To find all posts by an author with a given name, the best approach would be to find a user with its name and request all users’ posts:

$posts = Post::whereHas('author', function ($q) {
    $q->where('name', 'Karim');
})->get();

ORM: Symfony vs. Laravel

With regard to ORM, Eloquent looks much more friendly for PHP developers and easier to learn than Doctrine.

Event Dispatcher vs. Middleware

Symfony vs. Laravel Lifecycle

One of the most important things to understand about a framework is its lifecycle.

Symfony and Event Dispatcher

To convert a request into a response, Symfony uses EventDispatcher. It consequentially fires different lifecycle events and special event listeners to handle these events. In the beginning, it dispatches the kernel.request event that includes request information. The main default listener of this event is RouterListener, which invokes the router component to find a suitable route rule for the current request. After this, other events are executed step-by-step. Typical event listeners are a Security check, CSRF token verification, and a logging process. If you want to add some functionality in the request lifecycle, you need to create a custom EventListener and subscribe it to the necessary event.

Laravel and Middleware

Laravel uses a different solution: middleware. I like comparing middleware to an onion: Your application has certain layers and a request passes through these layers on the way to the controller and back. So, if you want to extend your application logic and add some functionality in the request lifecycle, you need to add an additional layer to your middleware list, and Laravel will execute it.

REST API

Let’s try to create a basic CRUD example to manage a blog post:

  • Create - POST /posts/
  • Read - GET /posts/{id}
  • Update - PATCH /posts/{id}
  • Delete - DELETE /posts/{id}

REST API in Symfony

Symfony doesn’t have an easy out-of-the-box solution for fast REST API creation, but it has great third-party bundles FOSRestBundle and JMSSerializerBundle.

Let’s consider the minimal working example with FOSRestBundle and JMSSerializerBundle. After you installed them and turned them on in AppKernel, you can set in the bundle configuration that you will use JSON format and that this doesn’t have to be included in the URL requests:

#app/config/config.yml
fos_rest:
    routing_loader:
        default_format: json
        include_format: false

In the routing configuration, you should specify that this controller will implement a REST resource:

#app/config/routing.yml
blog:
    resource: BlogBundle\Controller\PostController
    type:     rest

You implemented a persist method in the repository in the previous example; now you need to add a delete method:

// src/BlogBundle/Repository/PostRepository.php
public function delete(Post $post)
{
    $this->getEntityManager()->remove($post);
    $this->getEntityManager()->flush();
}

Next, you need to create a form class to accept input requests and map them to the model. You can do it by using a CLI helper:

php bin/console doctrine:generate:form BlogBundle:Post

You will receive a generated form type with the following code:

<?php
// src/BlogBundle/Form/PostType.php
namespace BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('title')->add('content');
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'BlogBundle\Entity\Post',
            'csrf_protection' => false
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'post';
    }
}

Now let’s implement our controller.

Note: the code I am going to show you is not perfect. It violates some design principles but could be easily refactored. The main purpose is to show you how to implement each method, step by step.

<?php
// src/BlogBundle/Controller/PostController.php
namespace BlogBundle\Controller;

use BlogBundle\Entity\Post;
use BlogBundle\Form\PostType;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class PostController extends FOSRestController
{
    /**
     * @param $id
     * @return Response
     */
    public function getPostAction($id)
    {
        $view = new View();
        $post = $this->getDoctrine()->getRepository('BlogBundle:Post')->find($id);
        if ($post === null) {
            $view->setStatusCode(Response::HTTP_NOT_FOUND);
        } else {
            $view->setData(['post' => $post]);
        }

        return $this->handleView($view);
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function postPostAction(Request $request)
    {
        $view = new View(null, Response::HTTP_BAD_REQUEST);
        $post = new Post;
        $form = $this->createForm(PostType::class, $post, ['method' => $request->getMethod()]);
        $form->handleRequest($request);
        if ($form->isValid()) {
            $this->getDoctrine()->getRepository('BlogBundle:Post')->persist($post);
            $view->setStatusCode(Response::HTTP_CREATED);
            $postUrl = $this->generateUrl('get_post', ['id' => $post->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
            $view->setHeader('Location', $postUrl);
        } else {
            $view->setData($form->getErrors());
        }

        return $this->handleView($view);
    }

    /**
     * @param $id
     * @param Request $request
     * @return Response
     */
    public function patchPostAction($id, Request $request)
    {
        $view = new View(null, Response::HTTP_BAD_REQUEST);
        $post = $this->getDoctrine()->getRepository('BlogBundle:Post')->find($id);
        if ($post === null) {
            $view->setStatusCode(Response::HTTP_NOT_FOUND);
        } else {
            $form = $this->createForm(PostType::class, $post, ['method' => $request->getMethod()]);
            $form->handleRequest($request);
            if ($form->isValid()) {
                $this->getDoctrine()->getRepository('BlogBundle:Post')->persist($post);
                $view->setStatusCode(Response::HTTP_NO_CONTENT);
                $postUrl = $this->generateUrl('get_post', ['id' => $post->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
                $view->setHeader('Location', $postUrl);
            } else {
                $view->setData($form->getErrors());
            }
        }

        return $this->handleView($view);
    }

    /**
     * @param $id
     * @return Response
     */
    public function deletePostAction($id)
    {
        $view = new View(null, Response::HTTP_NOT_FOUND);
        $post = $this->getDoctrine()->getRepository('BlogBundle:Post')->find($id);
        if ($post !== null) {
            $this->getDoctrine()->getRepository('BlogBundle:Post')->delete($post);
            $view->setStatusCode(Response::HTTP_NO_CONTENT);
        }

        return $this->handleView($view);
    }
}

With FOSRestBundle, you don’t need to declare a route for each method; just follow the convention with controller method names, and JMSSerializerBundle will automatically convert your models to JSON.

REST API in Laravel

First, you need to define routes. You can do this in the API section of the route rules to turn off some default middleware components and turn on others. The API section is located in the routes/api.php file.

<?php
// routes/api.php
Route::resource('/posts', 'BlogController');

In the model, you should define the $fillable property to pass variables in the model’s create and update methods:

<?php
// app/Post.php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'content'];
    // …

Now let’s define the controller:

<?php
// app/Http/Controllers/BlogController.php
namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class BlogController extends Controller
{
    public function show(Post $post)
    {
        return $post;
    }

    public function store(Request $request)
    {
        $post = Post::create($request->get('post'));
        return response(null, Response::HTTP_CREATED, ['Location'=>'/posts/'.$post->id]);
    }

    public function update(Post $post, Request $request)
    {
        $post->update($request->get('post'));
        return response(null, Response::HTTP_NO_CONTENT, ['Location'=>'/posts/'.$post->id]);
    }

    public function destroy(Post $post)
    {
        $post->delete();
        return response(null, Response::HTTP_NO_CONTENT);
    }
}

In Symfony, you are using FosRestBundle, which wrapped errors in JSON. In Laravel, you need to do it yourself. You need to update the render method in the Exception handler to return JSON errors for expecting JSON requests:

<?php
// app/Exceptions/Handler.php
namespace App\Exceptions;

use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Exception $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if ($request->expectsJson()) {
            $status = 400;
            if ($this->isHttpException($exception)) {
                $status = $exception->getStatusCode();
            } elseif ($exception instanceof ModelNotFoundException) {
                $status = 404;
            }

            $response = ['message' => $exception->getMessage(), 'code' => $exception->getCode()];

            return response()->json($response, $status);
        }

        return parent::render($request, $exception);
    }
    // ...
}

REST API: Symfony vs. Laravel

As you can see, for a typical REST API, Laravel is much simpler than Symfony.

Picking the Winner: Symfony or Laravel?

There’s no clear winner between Laravel and Symfony, as everything depends on your final goal. (A few Laravel and Symfony alternatives have also emerged over the years, but they are beyond the scope of this comparison.)

Laravel is a better choice if:

  • This is your first experience with the framework, as it’s easy to learn and has a simpler syntax and better learning materials.
  • You’re building a startup product and checking your hypothesis, as it’s good for rapid application development and Laravel developers are easy to find.

Symfony is the best option if:

  • You’re building a complex enterprise application, as it is very scalable, maintainable, and well structured.
  • You’re building a migration of a big long-term project, as Symfony has predictable release plans for the next six years, so it’s less likely that there will be any surprises.
Hire a Toptal expert on this topic.
Hire Now
Karim Sakhibgareev

Karim Sakhibgareev

Verified Expert in Engineering
20 Years of Experience

Porto, Portugal

Member since August 22, 2016

About the author

Having worked with Laravel for Media.net and with Symfony for Into Film, Karim is acquainted with the pros and cons of both frameworks.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.