Vse o WEB
Информация и размышления о Web технологиях

Silex - организация приложения

Всем привет! Сегодня мы продолжаем тему Silex и поговорим об организации приложения. Когда приложение начинает расти, становится не удобно держать все роуты и контроллеры в одном файле. К тому же нам могут понадобится сторонние библиотеки, расширяющие функционал нашего приложения. Несмотря на то, что Silex призван быть компактным и дает нам свободу в том, как мы будем писать наш код, мы можем также писать большие приложения на его основе. О том, как структурировать код, мы и поговорим в этой статье.

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

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

 

CREATE TABLE IF NOT EXISTS categories (
  id INTEGER(10) PRIMARY KEY AUTO_INCREMENT,
  slug VARCHAR(255) NOT NULL,
  name VARCHAR(30) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

CREATE TABLE IF NOT EXISTS articles (
  id INTEGER(10) PRIMARY KEY AUTO_INCREMENT,
  slug VARCHAR(255) NOT NULL,
  title VARCHAR(255) NOT NULL,
  shortDescription TEXT NOT NULL,
  content TEXT NOT NULL,
  price FLOAT DEFAULT 0,
  authorId INTEGER(10) NOT NULL,
  categoryId INTEGER(10),
  FOREIGN KEY (id) REFERENCES categories(id)
) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

CREATE TABLE IF NOT EXISTS authors (
  id INTEGER(10) PRIMARY KEY AUTO_INCREMENT,
  slug VARCHAR(255) NOT NULL,
  firstName VARCHAR(20) NOT NULL,
  lastName VARCHAR(20) NOT NULL,
  about TEXT
) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

# Many to Many: Articles-Authors
CREATE TABLE articles_authors (
  articleId INTEGER(10) NOT NULL,
  authorId INTEGER(10) NOT NULL,
  PRIMARY KEY (articleId, authorId),
  FOREIGN KEY (articleId) REFERENCES articles(id),
  FOREIGN KEY (authorId) REFERENCES authors(id)
) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

 

В нашем приложении будут присутствовать сущности статьи, категории и автора. Связь категории со статьей - One To Many (один ко многим) - у статьи может быть только одна категория, а в категории может быть сколько много статей. Связь статьи с автором - Many To Many (многие ко многим) - у статьи может быть много авторов, у автора может быть много статей.

Поскольку нашему приложению необходимо соединение с базой данных, давайте настроим его. Но прежде необходимо создать конфигурационные файлы для нашего приложения. Для этого будем использовать Igorw\Silex\ConfigServiceProvider и MJanssen\Provider\RoutingServiceProvider. Важно отметить, что эти провайдеры требуют для работы версию Silex 1.*. Установим их, используя composer:

 

php composer.phar require igorw/config-service-provider marcojanssen/silex-routing-service-provider

 

После этого создаем папку app/configs, а в ней два файла конфигурации: routes.php, application.php. К содержимому этих файлов мы вернемся позже. А пока обновим файл app/Application.php:

 

<?php

/**
 * Class Application
 */
class Application extends Silex\Application
{
    use \Silex\Application\TwigTrait;

    public function __construct(array $values = array())
    {
        parent::__construct($values);

        $this->bootstrapAppConfig($values);
        $this->bootstrapRoutesConfig();
        $this->bootstrapTwig();

    }

    /**
     * @param array $values
     * @throws HttpRuntimeException
     */
    public function bootstrapAppConfig(array $values)
    {
        if (empty($values['config'])) {
            throw new HttpRuntimeException('Application config is not set!');
        }

        $this->register(new \Igorw\Silex\ConfigServiceProvider($values['config']));
    }

    public function bootstrapRoutesConfig()
    {
        $this->register(new \Igorw\Silex\ConfigServiceProvider(__DIR__ . '/configs/routes.php'));
        $this->register(new \MJanssen\Provider\RoutingServiceProvider('config.routes'));
    }

   // ...
}

 

Создадим папку  /src/AppBundle/Controllers (позаимствуем идею из структуры приложения Symfony) и наш первый контроллер и view:

 

// src/AppBundle/Controller/IndexController.php

<?php

namespace AppBundle\Controller;

use Composer\DependencyResolver\Request;

class IndexController
{
    public function indexAction(Request $request, \Application $app)
    {
        return $app->render('index/index.html.twig');
    }
}

 

// app/Resources/views/index/index.html.twig

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>First Silex application!</title>
    </head>
    <body>
        <h1>Homepage</h1>
    </body>
</html>

 

Что бы это все заработало, нам нужно добавить область имен AppBundle в автозагрузку. Открываем файл composer.json и вносим правки:

 

{
  "require": {
    "silex/silex": "~1.3",
    "symfony/twig-bridge": "*",
    "twig/twig": "*",
    "igorw/config-service-provider": "^1.2",
    "marcojanssen/silex-routing-service-provider": "^1.5"
  },

  "autoload": {
    "psr-0": {
      "AppBundle" : "src/"
    }
  }
}

 

Далее выполняем

 

php composer.phar update

 

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

 

// app/configs/routes.php 

<?php

return array(
    'config.routes' => array(
          array(
              'name' => 'homepage',
              'pattern' => '/',
              'controller' => 'AppBundle\Controller\IndexController::indexAction',
              'method' => 'get'
          )
    ),
);

 

// web/index.php

<?php

require_once __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../app/Application.php';

$app = new Application(array('config' => __DIR__ . '/../app/configs/application.php'));

# enable debug mode
$app['debug'] = true;

$app->run();

 

Далее для работы нам понадобится соединение с БД. Внесем настройки в файл концигурации:

 

<?php

return array(
    'db.options' => array(
        'driver' => 'pdo_mysql',
        'user' => 'db_user',
        'password' => 'db_password',
        'host' => 'localhost',
        'dbname' => 'silex_journals',
        'charset' => 'utf8'
    )
);

 

Теперь нам нужен Silex\Provider\DoctrineServiceProvider. Установим его и зарегистрируем в приложении:

 

php composer.phar doctrine/dbal

 

// app/Application.php

<?php

/**
 * Class Application
 */
class Application extends Silex\Application
{
    use \Silex\Application\TwigTrait;

    public function __construct(array $values = array())
    {
        parent::__construct($values);

        $this->bootstrapAppConfig($values);
        $this->bootstrapRoutesConfig();
        $this->bootstrapTwig();
        $this->bootstrapDb();

    }

    // ...

    public function bootstrapDb()
    {
        $this->register(new \Silex\Provider\DoctrineServiceProvider(), array(
            'db.options' => $this['db.options']
        ));
    }
}

 

Теперь у нас все готово, можно заняться внешним видом приложения и изменить контроллер. Для генерации URL из контроллера установим - Silex\Provider\UrlGeneratorServiceProvider.

 

// app/Application.php

<?php

/**
 * Class Application
 */
class Application extends Silex\Application
{
    use \Silex\Application\TwigTrait;
    use \Silex\Application\UrlGeneratorTrait;

    public function __construct(array $values = array())
    {
        parent::__construct($values);

        // ...

        # bootstrap url generator 
        $this->bootstrapUrlGenerator();

        // ...

    }

    // ...

    public function bootstrapUrlGenerator()
    {
        $this->register(new \Silex\Provider\UrlGeneratorServiceProvider());
    }
}

 

Возможно, в будущем нам понадобится перевести наше приложение на несколько языков. Поэтому хорошо бы сразу все строки в шаблонах указывать в виде ключей. Сразу подключим и Silex\Provider\TranslationServiceProvider:

 

php composer.phar require symfony/translation symfony/config symfony/yaml

 

Теперь мы можем создать файл с переводами в формате yaml  и зарегистрировать наш провайдер переводов:

 

// app/Application.php

<?php

/**
 * Class Application
 */
class Application extends Silex\Application
{
    use \Silex\Application\TwigTrait;
    use \Silex\Application\UrlGeneratorTrait;

    public function __construct(array $values = array())
    {
        parent::__construct($values);

        // ...

        $this->bootstrapTranslations();
    }

    // ...

    public function bootstrapTranslations()
    {
        $this->register(new \Silex\Provider\TranslationServiceProvider(), array(
            'locale_fallback' => array('en')
        ));

        $app = $this;

        $this['translator'] = $this->share($this->extend('translator', function ($translator, $app) {
            $translator->addLoader('yaml', new \Symfony\Component\Translation\Loader\YamlFileLoader());
            $translator->addResource('yaml', __DIR__ . '/Resources/translations/messages.en.yml', 'en');

            return $translator;
        }));
    }
}

 

// app/Resources/translations/messages.en.yml

title:
  categories: Categories

article:
  no_articles: Sorry, articles haven't added yet.
  read: Read
  free_for_logged: FREE for logged!

 

Нашему приложению понадобится сессия. Регистрируем провайдер:

 

// app/Application.php

<?php

/**
 * Class Application
 */
class Application extends Silex\Application
{
    use \Silex\Application\TwigTrait;
    use \Silex\Application\UrlGeneratorTrait;

    public function __construct(array $values = array())
    {
        parent::__construct($values);

        // ...

        $this->bootstrapSession();

    }

    // ...

    public function bootstrapSession()
    {
        $this->register(new \Silex\Provider\SessionServiceProvider());
    }
}

 

Для работы с шаблонами нам понадобится добавить новую функцию Twig. Делается это достаточно просто:

 

// app/Application.php

// ...

public function bootstrapTwig()
{
    $this->register(new \Silex\Provider\TwigServiceProvider(), array(
        'twig.path' => __DIR__ . '/Resources/views'
    ));

    $this['twig'] = $this->share($this->extend('twig', function($twig, $this) {
        $app = $this; # save application context in variable

        $twig->addFunction(new Twig_SimpleFunction('asset', function($asset) use($app) {
            return $this['request']->getBaseUrl() . '/assets/' . $asset;
        }));

        return $twig;
    }));
}

// ...

 

Теперь мы можем обращаться к нашим ассетам используя функция asset.

 

// app/Resources/views/layout.html.twig

<!DOCTYPE html>
<html lang="en">
    <head>
        
        <!-- ....  -->

        {% block stylesheets %}
        <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
        <link href="{{ asset('css/bootstrap-theme.min.css') }}" rel="stylesheet">
        <link href="{{ asset('css/jumbotron-narrow.css') }}" rel="stylesheet">
        {% endblock %}

        <!-- ....  -->

    </head>

    <body>

        <!-- ....  -->

        {% block javascripts %}
        <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
        <script src="{{ asset('js/jquery-1.12.min.js') }}"></script>
        <script src="{{ asset('js/jquery-migrate-1.2.1.min.js') }}"></script>
        <script src="{{ asset('js/bootstrap.min.js') }}"></script>
        <script src="{{ asset('js/ie10-viewport-bug-workaround.js') }}"></script>
        {% endblock %}
    {% endblock %}
    </body>
</html>

 

Пришло время создать layout  нашего приложения. Я буду пользоваться шаблоном Jumbotron Narrow Bootstrap, а вы найдете содержимое этого файла в исходниках к статье.

Выводим список наших статей на главной странице. Для этого вносим изменения в котроллер и шаблон:

 

// src/AppBunble/Controller/IndexController.php

<?php

namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Request;

class IndexController
{
    /**
     * List of articles
     *
     * @param Request $request
     * @param \Application $app
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function indexAction(Request $request, \Application $app)
    {
        $query = <<<SQL
            SELECT a.slug as art_slug, a.title as art_title, a.shortDescription as art_short, a.price as art_price, a.published as art_published,
              c.name as cat_name, c.slug as cat_slug, au.slug as author_slug, CONCAT(au.firstName, ' ', au.lastName) as author_name
            FROM articles a
              JOIN categories c ON a.id = c.id
              JOIN articles_authors a_au ON a.id = a_au.articleId
              JOIN authors au ON a_au.authorId = au.id
            ORDER BY a.published DESC;  
SQL;
        
        $articles = $app['db']->executeQuery($query)->fetchAll();

        return $app->render('index/index.html.twig', array(
            'articles' => $articles
        ));
    }
}

 

{% extends 'layout.html.twig' %}

{% block content %}

    {% if articles is defined %}
        {% for article in articles %}
        <article>

            <h2>{{ article.art_title }}</h2>
            <section class="short-desc">{{ article.art_short|raw }}</section>
            <section class="meta">
                <i class="glyphicon glyphicon-user"></i>
                <a href="#">{{ article.author_name }}</a> |

                <i class="glyphicon glyphicon-folder-close"></i>
                <a href="#">{{ article.cat_name }}</a> |

                <i class="glyphicon glyphicon-calendar"></i>
                {{ article.art_published }}
            </section>
            <div>
                <a class="btn btn-success" href="#">{{ 'article.read'|trans }}</a>
            </div>
            {% if article.art_price == 0 %}
            <div class="remark free">{{ 'article.free_for_logged'|trans }}</div>
            {% endif %}
        </article>
        {% endfor %}
    {% else %}
    <p>{{ 'no_articles'|trans }}</p>
    {% endif %}
{% endblock %}

 

Запускаем приложение и смотрим, что у нас получилось:

 

Loading ...

 

Ну вот и все на сегодня. В следующей статье мы продолжим писать наше приложения. Разберемся как создать сущности для работы с БД и авторизацию пользователей.

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

 

Исходники: silex-app_sources.zip

Наверх