src: Laravel vs Symfony — 5 Шагов к Выбору PHP Фреймворка

Все мы знаем что современные веб-приложения и сайты создаются с помощью фреймворков. В то же время выбрать фреймворк для разработки часто бывает непросто. Если вы в поиске, предлагаю вам обратить внимание на Laravel и Symfony. В этой статье мы посмотрим насколько просто эти фреймворки помогают справляться с типичными задачами!


Последние несколько лет фреймворк Laravel приобретает все большую популярность и заставляет обратить на него внимание :). По опросу который проводил Sitepoint.com в конце 2013 года — Laravel занял первое место среди PHP фреймворков, обойдя Falcon и Symfony. Интересно, что Laravel использует внутри себя компоненты Symfony и это неудивительно, потому что компоненты Symfony пользуются большой популярностью, достаточно вспомнить хотя бы DependencyInjection. В то же время фреймворк Symfony также успешно развивается и остается на плаву.
Есть типовые задачи с которыми каждый день сталкиваются разработчики, предлагаю посмотреть насколько просто и легко популярные фреймворки помогают с ними справляться на примере Блога.
Тут надо сказать, что писать эту статью я начал до официального релиза Laravel 5, так что в распоряжении у нас:

  • Laravel 4.2
  • Symfony 2.6

Шаг 1. Установка и запуск

Все таки, согласитесь, здорово если у вас получается просто и быстро развернуть фреймворк на локальной машине или сервере и начать быстро работу? Если фреймворк позволяет это сделать — то это облегчает жизнь Вам и Вашей команде.

Laravel

В документации Laravel предлагается несколько вариантов установки фреймворка: Laravel Installer, Composer, а также простое клонирование репозитория из github. Так как мире PHP фреймоврков Composer, в последнее время, стал стандартом де-факто, будем использовать composer:

php composer.phar create-project laravel/laravel test-laravel

После установки, мы можем сразу же запустить встроенный built-in сервер, с помощью команды:

php artisan serve
Laravel development server started on http://localhost:8000

После открытия http://localhost:8000 в браузере, мы видим что Laravel готов к работе:

Laravel PHP Framework

В общем, все довольно просто и удобно. Да, и нужно сказать у Laravel имеется такая удобная вещь как Laravel Homestead, которая позволяет запустить готовое окружение для разработки. Homestead — это простая виртуальная машина, созданная специально для Laravel. Она включает в себя необходимые приложения: Ubuntu, PHP, nginx, mysql, даже redis и memcached. Правда, чтобы запустить всю эту красоту, вам понадобиться еще установить VirtualBox и Vagrant.

Symfony

В документации Symfony также предлагается несколько вариантов для установки: Symfony Installer или Composer. Будем снова использовать composer:

php composer.phar create-project symfony/framework-standard-edition test-symfony

После установки Symfony предлагает установить AcmeDemoBundle, для первого раз можно согласиться и ввести «y» :)

Would you like to install Acme demo bundle? [y/N] y 
Installing the Acme demo bundle

Стоит также обратиться внимание, что после этого вопроса Symfony сразу предлагает создать файл параметрами, там можно сразу указать данные для доступа к базе данных и другие настройки. Нужно отметить, что это удобно:

Creating the "app/config/parameters.yml" file
Some parameters are missing. Please provide them.
database_driver (pdo_mysql): 
...

Теперь наше приложение можно запустить:

php app/console server:run

После этого приложение будет доступно по адресу http://localhost:8000. Однако, мне пришлось запустить приложение по адресу http://localhost:8001, потому что 8000-й порт уже занял Laravel :).

Теперь приложение доступно в браузере http://localhost:8001:

symfony-welcome

Резюме

  • установить Symfony также просто как и Laravel, однако, все-таки, я бы отдал должное Symfony, так как после установки, у нас уже настроено соединение с базой и никаких дополнительных манипуляций делать не нужно.

Шаг 2. Создание сущностей (моделей)

Самая распространенная типовая задача на любом сайте или веб приложении — это сохранение объектов в базу данных и чтение объектов из базы данных.

Laravel  и Symfony предлагают разработчикам ORM системы из-коробки. В случае с Laravel это Eloquent ORM и в случае Symfony это Doctrine2 ORM. Eloquent и Doctrine используют разные паттерны для доступа к базе данных. Eloquent реализует шаблон Active Record, а Doctrine использует достаточно известные Data MapperUnit Of Work и Identity Map.

Laravel

В Laravel нет из коробки команды которая бы создала нам базу данных. Нам придется вручную создавать базу данных.

Наши модели в Laravel будут выглядеть так:

Статья — связь один-ко-многим (одна статья может иметь много комментариев)

// app/models/Article.php

class Article extends Eloquent
{
    protected $fillable = ['title', 'text'];

    public function comments()
    {
        return $this->hasMany('Comment');
    }
}

Комментарий — многие-ко-одному (много комментариев может быть привязано к одной статье):

// app/models/Comment.php

class Comment extends Eloquent
{
    protected $fillable = ['title', 'text'];

    public function article()
    {
        return $this->belongsTo('Article');
    }
}

Запустим команду миграции, которая создаст PHP класс схемы таблиц для статей и комментариев:

php artisan migrate:make create_articles_table --table=articles --create=articles
php artisan migrate:make create_comments_table --table=comments --create=comments

После добавления нужных полей в файл миграции, запустить миграцию:

php artisan migrate

Создать статью, привязать к ней комментарий и сохранить их в базу данных мы можем так:

// app/routes.php
Route::get('articles/create', function() {
    $article = new Article();
    $article->title = 'Title at ' . date('Y-m-d h:i:s', time());
    $article->text = 'This is a simple text';
    $article->save();

    $comment = new Comment();
    $comment->user_name = 'John';
    $comment->text = 'some comment';

    $article->comments()->save($comment);

    return $article->id;
});

Symfony

В отличии от Laravel, в Symfony благодаря SensioGeneratorBundle есть команды для создания/удаления базы данных и генерации сущностей:

Создать базу данных мы можем, используя команду:

php app/console doctrine:database:create

Благодаря команде generate:doctrine:entity мы можем создать сущность, указать для нее бандл, конфигурацию маппинга и свойства которыми должна обладать наша сущность.

php app/console generate:doctrine:entity

Статья — связь один-ко-многим (одна статья может иметь много комментариев)

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

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

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

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

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Comment", mappedBy="article")
     */
    private $comments;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set title
     *
     * @param string $title
     * @return Article
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string 
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set text
     *
     * @param string $text
     * @return Article
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get text
     *
     * @return string 
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set comments
     *
     * @param ArrayCollection $comments
     * @return Article
     */
    public function setComments($comments)
    {
        $this->comments = $comments;

        return $this;
    }

    /**
     * Get comments
     *
     * @return ArrayCollection
     */
    public function getComments()
    {
        return $this->comments;
    }
}

Комментарий — многие-к-одному (много комментариев может быть привязано к одной статье):

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

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

    /**
     * @var string
     *
     * @ORM\Column(name="user_name", type="string", length=255)
     */
    private $userName;

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

    /**
     * @var Article
     *
     * @ORM\ManyToOne(targetEntity="Article", inversedBy="Comments")
     */
    private $article;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set userName
     *
     * @param string $userName
     * @return Comment
     */
    public function setUserName($userName)
    {
        $this->userName = $userName;

        return $this;
    }

    /**
     * Get userName
     *
     * @return string 
     */
    public function getUserName()
    {
        return $this->userName;
    }

    /**
     * Set text
     *
     * @param string $text
     * @return Comment
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get text
     *
     * @return string 
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set article
     *
     * @param Article $article
     * @return Comment
     */
    public function setArticle(Article $article)
    {
        $this->article = $article;

        return $this;
    }

    /**
     * Get article
     *
     * @return Article
     */
    public function getArticle()
    {
        return $this->article;
    }
}

Также в Symfony имеется такая команда как doctrine:generate:entities которую можно запустить после того как мы укажем взаимосвязи между сущностями. Эта команда создаст дополнительные методы которые нужны для связей между сущностями. Правда, на практике использовать эту команду нужно осторожно, потому что у вас может появится масса методов которые в итоге станут мертвым кодом.

Чтобы наши объекты можно было сохранять в базу данных мы можем запустить команду которая обновит схему базы данных:

php app/console doctrine:schema:update --force

Использовать эту команду в продакшене не рекомендуется.

В Symfony нет встроенных инструментов для управления миграциями базы данных и правильное решение — это использование DoctrineMigrationsBundle, который создает файлы миграций схемы базы данных и дает возможность откатываться на нужную версию. Правда, надо отметить, это не всегда возможно. :)

Создать нашу статью и привязать к ней комментарий мы можем так:

<?php

namespace Acme\DemoBundle\Controller;

use Acme\DemoBundle\Entity\Article;
use Acme\DemoBundle\Entity\Comment;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Response;

class ArticleController extends Controller
{
    /**
     * @Route("/article/create", name="_article_create")
     * @Template()
     */
    public function createAction()
    {
        $article = new Article();
        $article->setTitle('Simple title');
        $article->setText('Simple text');

        $comment = new Comment();
        $comment->setText('This is comment text');
        $comment->setArticle($article);
        $comment->setUserName('John');

        $em = $this->getDoctrine()->getManager();
        $em->persist($article);
        $em->persist($comment);
        $em->flush();

        return new Response('Article id: ' . $article->getId() . ', Comment id: ' . $comment->getId());
    }
}

Резюме

  • Laravel из коробки не имеет инструментов для того, чтобы автоматически менять схему базы данных. Когда Вы разрабатываете вашу модель и добавляете в нее новые свойства, вам необходимо всякое новое свойство добавлять в класс миграции. Хотелось бы иметь более умный генератор миграций из-коробки.
  • В Laravel есть возможность генерировать restful контроллер, но опять таки, это будет только заготовка с пустыми экшенами. Если вам нужен скафолдинг генератор для того, чтобы сгенерировать рабочий кусок приложения, посмотрите на популярный Laravel-4-Generators который умеет также генерировать миграции, модели, контроллеры и т.п.
  • Symfony как-то больше радует в плане возможностей генераторов которые поставляются вместе с фреймворком. Symfony имеет достаточно удобный генератор для сущностей и для изменения схемы базы данных, а также хороший инструмент для скафолдинга, который может сгенерировать не просто контроллер с пустыми экшенами, но более менее рабочий кусок приложения. Команда doctrine:generate:crud создает CRUD контроллер и форму, основываясь на Doctrine сущности.
  • Модели в Laravel смотрятся довольно элегантно, но за этой магией, лаконичностью и краткостью, на мой взгляд, кроется и большой недостаток. Из-за отсутствия геттеров/сеттеров, мы не имеем автодополнения в IDE, что делает разработку менее удобной. Чтобы иметь автодополнение кода в среде разработки нужно будет сделать несколько дополнительных телодвижений: либо вручную указывать док блоки @property для классов либо использовать популярный laravel-ide-helper, который позволяет генерировать phpdocs к моделям автоматически.
  • Паттерны которые использует Doctrine — это очень гибкий подход к источникам данных. Если сильно захотеть в Doctrine можно написать кастомный репозиторий который никак не будет связан с mysql базой данных и получать сущности вообще откуда угодно, из файлов, документов или из сторонних сервисов. Да, может быть, перед вами и не будет стоять такая задача, но сама возможность создать такой репозиторий без написания дополнительных сервисов — радует. Идея в том, что можно максимально абстрагироваться от источников данных и заниматься бизнес логикой.
  • Active Record, лежащий в основе моделей Laravel — это, конечно, очень удобный способ работы с объектами, но сам тот факт, что логика сохранения модели в базу данных лежит в самой модели может накладывать некоторые ограничения в будущем. С другой стороны, я пока не слышал о случаях когда Active Record действительно портил бы жизнь разработчикам :)

Шаг 3. Service Container

Думаю, вы согласитесь, что работа с зависимостями и сервисами — это тоже одна из частых задач с которыми мы постоянно сталкиваемся. Нам часто приходится создавать свои или использовать сторонние сервисы и важно чтобы фреймворк позволял быстро и удобно с ними работать.

В веб-разработке сложно что-то объяснить если не приводить примеры. Давайте просто посмотрим как фреймворки предлагают работать с контейнером сервисов.

Laravel

В Laravel есть несколько способов определить сервис, но хорошей практикой считается инициализация сервиса через сервис провайдер. Провайдер удобно использовать когда вы хотите группировать несколько сервисов в одном месте и просто чтобы держать код более структурированным.

Кстати, я был немного разочарован, когда пришлось потратить много времени на то, чтобы заставить свой сервис провайдер работать. Мне пришлось еще отдельно настроить автозагрузку классов и сказать об этом в файле composer.json.

Создадим сервис RssFeed

<?php

namespace Acme\Rss;

class RssFeed
{
    public function getItems()
    {
        return ['item 1', 'item 2'];
    }
}

в RssProvider, определим сервис rss.feed

<?php
//app/Acme/Providers/RssProvider 
namespace Acme\Providers;

use Acme\Rss\RssFeed;
use Illuminate\Support\ServiceProvider;

class RssProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('rss.feed', function() {
            return new RssFeed();
        });
    }
}

Также нужно добавить имя сервиса вместе с неймспейсом «Acme\Providers\RssProvider» в файл app/config.app.php в секцию «providers»

Вызываем сервис

Route::get('services', function() {
    $feed = App::Make('rss.feed');
    var_dump($feed->getItems());
});

Symfony

Сервисы в Symfony обычно располагаются в бандлах, например, в нашем AcmeBundle сервисы находятся в файле src/Acme/DemoBundle/Resources/config/services.yml. Symfony позволяет определять сервисы в форматах YAML, XML, PHP.

Создать сервис в формате YAML очень просто:

# src/Acme/DemoBundle/Resources/config/services.yml
services:
    rss.feed:
        class: Acme\DemoBundle\Rss\RssFeed

Несмотря на лаконичность формата YAML, лучше определять сервисы в XML формате. Благодаря XSD схеме, нормальная среда разработки будет автоматически делать валидацию ваших  сервисов в Symfony.

Вызываем сервис:

// src/Acme/DemoBundle/Controller/ArticleController.php

class ArticleController extends Controller
{
    /**
     * @Route("/rss", name="rss_feed")
     * @Template()
     */
    public function rssAction()
    {
        $feed = $this->get('rss.feed');

        return new Response(
            var_dump($feed->getItems())
        );
    }
}

Если вы хотите создать сервис который имеет зависимость от другого сервиса (например от Doctrine Entity Manager), нужно просто использовать секцию arguments:

# src/Acme/DemoBundle/Resources/config/services.yml
services:
    rss.feed:
        class: Acme\DemoBundle\Rss\RssFeed
        arguments: [ "@doctrine.orm.entity_manager" ]

Соответственно, в классе RssFeed нужно будет создать конструктор, который в качестве аргумента будет принимать менеджер сущностей:

class RssFeed
{
    /**
     * @var EntityManager
     */
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function getItems()
    {
        return ['item 1', 'item 2'];
    }
}

Резюме

  • Laravel и Symfony имеют хорошие реализации сервис контейнера. Но я бы отметил, что благодаря своей архитектуре Symfony может продемонстрировать более гибкий способ работы с контейнером. Например, когда вы устанавливаете какой-либо сторонний бандл, все что вам нужно сделать это просто подключить его в файле app/AppKernel.php. После этого вам становятся доступны все сервисы этого бандла.
  • В Symfony обращает на себя внимание интересный компонент Config, который позволяет создать вам свои собственные конфигурацию и валидацию для вашего бандла. Это очень удобно когда Вы пользуетесь сторонним бандлом или собираетесь отдать свой бандл в Open Source.

Шаг 4. Кеширование

Рано или поздно все мы сталкиваемся с проблемой оптимизации нашего веб-приложения. Конечно  нам хотелось бы, чтобы фреймворк эффективно помогал решать проблемы кеширования.

Laravel

Из коробки кеш Laravel умеет сериализовать массивы и объекты и сохранять их в файловой системе. Хорошая новость также состоит в том, что мы можем использовать такие популярные кешируюшие бекенды как Memcached and Redis.

Самый простой способ взять посты нашего блога и закешировать их на 10 минут:

// app/routes.php

Route::get('articles', function() {
    $articles = Cache::get('articles', function() {
        $articles = Article::all();
        Cache::add('articles', $articles, 10);

        return $articles;
    });

    foreach ($articles as $article) {
        var_dump($article->id, $article->title);
    }
});

Код, приведенный выше берет из базы данных все статьи и посредством замыкания кеширует их на 10 минут. По истечении 10 минут запрос в базу данных повторится.

Symfony

Symfony позиционирует себя как фреймворк который заимствует модель кеширования у гигантов. Кеширующие модели HTTP валидации и просроченности являются базовыми для Symfony и основуются на известной HTTP спецификации. Если мы используем Symfony Http Cache то страница сайта кешируется целиком и все пользователи какое-то время будут получать кешированную версию:

    /**
     * @Route("/articles", name="articles_index")
     * @Cache(expires="tomorrow", public=true)
     */
    public function articlesAction()
    {
        // code
    }

В данном случае когда мы зайдем на страницу «/articles» Symfony закеширует страницу со статьями до завтрашнего дня.

И еще более интересный вариант с условной инвалидацией кеша:

    /**
     * @Route("/article/{id}", name="show_article")
     * @Cache(lastModified="article.getUpdatedAt()", ETag="'Article' ~ article.getId() ~ article.getUpdatedAt()")
     * @Template()
     */
    public function articleAction(Article $article)
    {
        return new Response($article->getTitle());
    }

В данном случае Symfony не будет дергать articleAction и всегда будет возвращать закешированную версию страницу пока статья не обновится. Да, в этом случае каждый раз будет происходить, как минимум, один запрос в базу данных и возможно это не лучший вариант для нагруженных приложений. Более умный вариант инвалидации вы можете реализовать сами или воспользоваться таким интересным решением как FOSHttpCacheBundle.

Резюме

  • На мой взгляд, в Symfony подошли к вопросу кеширования более основательно. Благодаря аннотациям нам доступен очень удобный способ управлять кешем. Благодаря поддержке HTTP модели валидации и просроченности мы можем также увеличить скорость работы сайта за счет кеша браузера и использования сторонних прокси-серверов таких как Nginx и Varnish.
  • Надо сказать, что разработчики которые пишут на Laravel версии < 5 тоже предлагают решения для кеширования всей страницы посредством Laravel filters, а Laravel 5 уже поддерживает возможность кеширования роутинга.

Шаг 5. Производительность

Наверное, все согласятся, что производительность — один из важных моментов при выборе фреймворка. В этом тесте я использовал утилиту ab (Apache Benchmark). Symfony был запущен в prod окружении. Кеш не использовался. По роутингу «/articles» мы просто берем все статьи из базы данных (их всего 10) и отображаем в шаблоне.

ab -n 100 -c 100 http://localhost:8000/articles

Результаты тестирования

Laravel Symfony
Время затраченное на тесты (sec) 0.958 8.438
Запросов в секунду 104.43 11.85
Ожидание ответа сервера, минимальное (ms) 22 101
Ожидание ответа сервера, среднее (ms) 475 4313
Ожидание ответа сервера, максиммальное (ms) 953 8434
Скорость передачи (Kbytes/sec) 859.28 33.52

 

Резюме

Несмотря на то что фреймворки запускались на встроенных PHP серверах, Laravel оказался примерно в 10 раз быстрее чем Symfony. Из этого маленького теста уже понятно, что работая с Symfony вопрос с кешированием будет стоять особенно остро. К счастью, на первых порах, можно вполне неплохо оптимизировать приложение, используя Http Cache. В Laravel же, благодаря кешированию, можно будет достичь еще большей производительности.

Заключение

В моей статье, было многое не сказано. Думаю можно было бы написать еще 20 важных вещей которые очень влияют на выбор фреймворка, но для этого просто не хватило бы времени :) поэтому я написал о том, что мне показалось важным на тот момент.

Бытует мнение, что для каждого фреймворка есть своя ниша. Мне, например, кажется, что Laravel хорошо подходит для небольших проектов, а Symfony — отличный выбор для больших проектов и приложений уровня Enterprice.

Но в то же время, считаю, что тут есть одно «но». Если вы, например, выбрали Symfony для своего нового проекта и приобрели неплохой опыт работы с этим фреймворком, то я очень сомневаюсь, что у вас будет время и возможность изучить Laravel, чтобы использовать его для проектов по меньше, разве не так? Также если у вас есть хороший опыт работы с Laravel, я сомневаюсь, что вы захотите писать проект с более сложной бизнес-логикой на Symfony. Поэтому когда разработчик или команда разработчиков выбирает фреймоворк, то с большой долей вероятности они выбирают инструмент который будут использовать всегда и на проектах разной сложности.

Друзья, надеюсь, мне удалось помочь вам в выборе фреймворка. Если Вы выбираете инструмент для постоянной разработки или присматриваетесь к Laravel или Symfony — желаю вам не ошибиться!

Комментарии запрещены.