Управление ресурсами

Phalcon\Assets — это компонент позволяющий разработчику управлять статичными ресурсами в веб-приложении, такими как каскадные таблицы стилей или скрипты.

Phalcon\Assets\Manager доступен в контейнере сервисов и вы можете добавлять ресурсы из любой части приложения, где контейнер доступен.

Добавление ресурсов

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

Добавить ресурсы в эти коллекции очень просто:

 

<?php

use Phalcon\Mvc\Controller;

class IndexController extends Controller
{
    public function indexAction()
    {
        // Добавляем некоторые локальные CSS ресурсы
        $this->assets->addCss('css/style.css');
        $this->assets->addCss('css/index.css');

        // А теперь некоторые локальные JavaScript ресурсы
        $this->assets->addJs('js/jquery.js');
        $this->assets->addJs('js/bootstrap.min.js');
    }
}

Далее, добавленные ресурсы могут быть отображены в представлениях:

<html>
    <head>
        <title>Некоторый удивительный веб-сайт</title>

        <?php $this->assets->outputCss(); ?>
    </head>

    <body>
        <!-- ... -->

        <?php $this->assets->outputJs(); ?>
    </body>
<html>

С использованием синтаксиса Volt:

<html>
    <head>
        <title>Некоторый удивительный веб-сайт</title>

        {{ assets.outputCss() }}
    </head>

    <body>
        <!-- ... -->

        {{ assets.outputJs() }}
    </body>
<html>

В целях достижения лучшей производительности, рекомендуется размещать загрузку JavaScript-скриптов в конце страницы, а не в <head>.

Локальные/удалённые ресурсы

Локальные ресурсы это те, которые предоставляются вами в том же приложении. Обычно они расположены в корневом каталоге приложения. Ссылки на локальные ресурсы генерируются с помощью сервиса url, чаще с применением Phalcon\Mvc\Url.

Удалённые ресурсы, такие как общие библиотеки, например jQuery, Bootstrap или пр., предоставляемые посредством CDN.

Второй параметр методов addCss() и addJs() говорит является ли ресурс локальным или нет (если true, то локальный, false — удалённый). По умолчанию, менеджер ресурсов будет предполагать, что ресурс является локальным:

<?php

public function indexAction()
{
    // Добавляем некоторые удалённые CSS ресурсы
    $this->assets->addCss('//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css', false);

    // А теперь, некоторые локальные CSS ресурсы
    $this->assets->addCss('css/style.css', true);
    $this->assets->addCss('css/extra.css');
}

Коллекции

В коллекциях группируются однотипные ресурсы. Менеджер ресурсов всегда создает две коллекции: css и js. Для группирования специфичных ресурсов вы можете создавать дополнительные:

<?php

// JavaScript ресурсы в коллекии header
$headerCollection = $this->assets->collection('header');

$headerCollection->addJs('js/jquery.js');
$headerCollection->addJs('js/bootstrap.min.js');

// JavaScript ресурсы в коллекии footer
$footerCollection = $this->assets->collection('footer');

$footerCollection->addJs('js/jquery.js');
$footerCollection->addJs('js/bootstrap.min.js');

Затем в представлении:

<html>
    <head>
        <title>Некоторый удивительный веб-сайт</title>

        <?php $this->assets->outputJs('header'); ?>
    </head>

    <body>
        <!-- ... -->

        <?php $this->assets->outputJs('footer'); ?>
    </body>
<html>

Синтаксис шаблонизатора Volt:

<html>
    <head>
        <title>Некоторый удивительный веб-сайт</title>

        {{ assets.outputCss('header') }}
    </head>

    <body>
        <!-- ... -->

        {{ assets.outputJs('footer') }}
    </body>
<html>

URL префиксы

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

<?php

$footerCollection = $this->assets->collection('footer');

if ($config->environment === 'development') {
    $footerCollection->setPrefix('/');
} else {
    $footerCollection->setPrefix('http:://cdn.example.com/');
}

$footerCollection->addJs('js/jquery.js');
$footerCollection->addJs('js/bootstrap.min.js');

Также, доступен синтаксис цепочки (Method chaining):

<?php

$headerCollection = $assets
    ->collection('header')
    ->setPrefix('http://cdn.example.com/')
    ->setLocal(false)
    ->addJs('js/jquery.js')
    ->addJs('js/bootstrap.min.js');

Минификация/Фильтрация

Phalcon\Assets предоставляет встроенную возможность минимизации JavaScript и CSS. Разработчик может создать коллекцию ресурсов, с указаниями для менеджера ресурсов, к каким ресурсам должны быть применены фильтры, а к каким нет. В дополнении к вышесказанному, Jsmin Дугласа Крокфорда (Douglas Crockford) входит в состав ядра минимизации JavaScript для увеличения производительности. Для минимизации CSS используется CSSMin Райна Дэйя (Ryan Day).

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

<?php

$manager

    // Этот JavaScript расположен внизу страницы
    ->collection('jsFooter')

    // Название получаемого файла
    ->setTargetPath('final.js')

    // С таким URI генерируется тэг HTML
    ->setTargetUri('production/final.js')

    // Это удалённый ресурс, не нуждающийся в фильтрации
    ->addJs('code.jquery.com/jquery-1.10.0.min.js', false, false)

    // Это локальные ресурсы, к которым необходимо применить фильтры
    ->addJs('common-functions.js')
    ->addJs('page-functions.js')

    // Объединяем все ресурсы в один файл
    ->join(true)

    // Используем встроенный фильтр Jsmin
    ->addFilter(
        new Phalcon\Assets\Filters\Jsmin()
    )

    // Используем пользовательский фильтр
    ->addFilter(
        new MyApp\Assets\Filters\LicenseStamper()
    );

Коллекция может содержать либо JavaScript, либо CSS ресурсы, но не оба типа ресурсов сразу. Некоторые ресурсы могут быть удалёнными, то есть, полученными с помощью HTTP-запроса для дальнейшей фильтрации. Рекомендуется преобразовывать внешние ресурсы в локальные, для устранения накладных расходов на их получение.

Как было замечено выше, метод addJs(), использующийся для добавления ресурсов в коллекцию, в качестве второго принимает параметр, указывающий является ли ресурс внешним или нет, а также третий параметр, указывающий, следует ли применять фильтр к ресурсу или оставить как есть:

<?php

// Эти JavaScript ресурсы расположены внизу
$jsFooterCollection = $manager->collection('jsFooter');

// Это удалённый ресурс, не нуждающийся в фильтрации
$jsFooterCollection->addJs('code.jquery.com/jquery-1.10.0.min.js', false, false);

// Это локальные ресурсы, к которым необходимо применить фильтры
$jsFooterCollection->addJs('common-functions.js');
$jsFooterCollection->addJs('page-functions.js');

Фильтры регистрируются в коллекции. Допускается регистрировать несколько фильтров. Ресурсы в наборе фильтруются в том же порядке, в каком были зарегистрированы фильтры:

<?php

// Используем встроенный фильтр Jsmin
$jsFooterCollection->addFilter(
    new Phalcon\Assets\Filters\Jsmin()
);

// Используем пользовательский фильтр
$jsFooterCollection->addFilter(
    new MyApp\Assets\Filters\LicenseStamper()
);

Обратите внимание, встроенные и пользовательские фильтры могут прозрачно применяться ко всей коллекции ресурсов. Последний шаг, определяет, стоит ли объединять все ресурсы набора в один файл, или использовать каждый по отдельности. Если все ресурсы набора должны объединяться в один файл, вы можете использовать метод join().

Если ресурсы должны быть объединены, то вы должны также определить какой файл будет использоваться для хранения ресурсов и по какому URI он будет доступен. Эти параметры настраиваются при помощи методов setTargetPath() и setTargetUri():

<?php

$jsFooterCollection->join(true);

// Название получаемого файла
$jsFooterCollection->setTargetPath('public/production/final.js');

// С таким URI генерируется тэг HTML
$jsFooterCollection->setTargetUri('production/final.js');

Встроенные фильтры

Phalcon предоставляет два встроенных фильтра минимизации JavaScript и CSS ресурсов. Их реализация на языке Си обеспечивает минимальные накладные расходы для решения подобной задачи:

ФильтрОписание
Phalcon\Assets\Filters\Jsmin Минимизирует JavaScript удаляя не нужны символы, которые игнорируются интерпретатором/компилятором JavaScript
Phalcon\Assets\Filters\Cssmin Минимизирует CSS удаляя ненужные символы, которые игнорируются браузерами

Пользовательские фильтры

Кроме использования встроенных фильтров, вы можете создавать свои собственные фильтры. Вы можете воспользоваться существующими более продвинутыми инструментами, такими как YUI, Sass, Closure и другие:

<?php

use Phalcon\Assets\FilterInterface;

/**
 * Фильтрация CSS содержимого при помощи YUI
 *
 * @param string $contents
 * @return string
 */
class CssYUICompressor implements FilterInterface
{
    protected $options;

    /**
     * Конструктор CssYUICompressor
     *
     * @param array $options
     */
    public function __construct(array $options)
    {
        $this->options = $options;
    }

    /**
     * Выполнение фильтрации
     *
     * @param string $contents
     *
     * @return string
     */
    public function filter($contents)
    {
        // Запись содержимого во временный файл
        file_put_contents('temp/my-temp-1.css', $contents);

        system(
            $this->options['java-bin'] .
            ' -jar ' .
            $this->options['yui'] .
            ' --type css ' .
            'temp/my-temp-file-1.css ' .
            $this->options['extra-options'] .
            ' -o temp/my-temp-file-2.css'
        );

        // Возвращаем содержимое файла
        return file_get_contents('temp/my-temp-file-2.css');
    }
}

Применение:

<?php

// Получаем некоторую CSS коллекцию
$css = $this->assets->get('head');

// Добавляем (включаем) фильтр YUI-компрессор к коллекции
$css->addFilter(
    new CssYUICompressor(
        [
            'java-bin'      => '/usr/local/bin/java',
            'yui'           => '/some/path/yuicompressor-x.y.z.jar',
            'extra-options' => '--charset utf8',
        ]
    )
);

В предыдущем примере мы использовали пользовательский фильтр, который назывался LicenseStamper:

<?php

use Phalcon\Assets\FilterInterface;

/**
 * Добавляет лицензионное собщение в начало файла
 *
 * @param string $contents
 *
 * @return string
 */
class LicenseStamper implements FilterInterface
{
    /**
     * Выполнение фильтрации
     *
     * @param string $contents
     * @return string
     */
    public function filter($contents)
    {
        $license = '/* (c) 2015 Ваше имя здесь */';

        return $license . PHP_EOL . PHP_EOL . $contents;
    }
}

Пользовательский вывод

Методы outputJs() и outputCss() создают необходимые HTML-тэги в соответствии с каждым типом ресурсов. Однако вы можете переопределить эти методы или выводить ресурсы вручную, используя следующий подход:

<?php

use Phalcon\Tag;

$jsCollection = $this->assets->collection('js');

foreach ($jsCollection as $resource) {
    echo Tag::javascriptInclude(
        $resource->getPath()
    );
}

Повышение производительности

Существует множество способов оптимизации ресурсов обработки. Ниже мы опишем простой метод, который позволяет обрабатывать ресурсы напрямую через веб-сервер для оптимизации времени отклика.

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

<?php

namespace App\Controllers;

use Phalcon\Mvc\Controller;
use Phalcon\Assets\Filters\Jsmin;

/**
 * App\Controllers\ControllerBase
 *
 * Это базовый контроллер для приложения.
 */
class ControllerBase extends Controller
{
    public function onConstruct()
    {
        $this->assets
            ->useImplicitOutput(false)
            ->collection('global')
            ->addJs('https://code.jquery.com/jquery-3.2.1.js', false, true)
            ->addFilter(new Jsmin());
    }
}

Затем, нам понадобится настроит маршрутизацию:

<?php
/*
 * Определяем маршруты.
 * Этот файл будет подключен при определении сервиса router.
 */
$router = new Phalcon\Mvc\Router();

$router->addGet('/assets/(css|js)/([\w.-]+)\.(css|js)', [
    'controller' => 'assets',
    'action'     => 'serve',
    'type'       => 1,
    'collection' => 2,
    'extension'  => 3,
]);

// Другие маршруты...

Наконец нам нужно создать контроллер для обработки запросов ресурсов:

<?php

namespace App\Controllers;

use Phalcon\Http\Response;

/**
 * Обработчик запросов ресурсов.
 */
class AssetsController extends ControllerBase
{
    public function serveAction() : Response
    {
        // Создаём экземпляр Response
        $response = new Response();

        // Подготавливаем пути
        $collectionName = $this->dispatcher->getParam('collection');
        $extension      = $this->dispatcher->getParam('extension');
        $type           = $this->dispatcher->getParam('type');
        $targetPath     = "assets/{$type}/{$collectionName}.{$extension}";

        // Настраиваем тип ответа
        $contentType = $type == 'js' ? 'application/javascript' : 'text/css';
        $response->setContentType($contentType, 'UTF-8');

        // Проверяем коллекцию на существование
        if (!$this->assets->exists($collectionName)) {
            return $response->setStatusCode(404, 'Not Found');
        }

        // Настраиваем коллекцию ресурсов
        $collection = $this->assets
            ->collection($collectionName)
            ->setTargetUri($targetPath)
            ->setTargetPath($targetPath);

        // Сохраняем содержимое на диск и возвращаем полный путь к сохранённому файлу
        $contentPath = $this->assets->output($collection, function (array $parameters) {
            return BASE_PATH . '/public/' . $parameters[0];
        }, $type);

        // Устанавливаем содержимое ответа
        $response->setContent(file_get_contents($contentPath));

        // Возвращаем объект Response
        return $response;
    }
}

сли обработанные ресурсы существуют на диске, они должны быть возвращены непосредственно веб-сервером. Таким образом, чтобы получить выгоду от работы со статикой, мы должны обновить конфигурацию веб-сервера. В примере ниже мы будем использовать конфигурацию для Nginx. Настройка других веб-серверов, например Apache, будет немного отличаться:

location ~ ^/assets/ {
    expires 1y;
    add_header Cache-Control public;
    add_header ETag "";

    # Если статический ресур существует, обработать его веб-сервером,
    # без запуска PHP-приложения
    try_files $uri $uri/ @phalcon;
}

location / {
    try_files $uri $uri/ @phalcon;
}

location @phalcon {
    rewrite ^(.*)$ /index.php?_url=$1;
}

Нам нужно создать assets/js и assets/css каталоги в корне документа приложения (например. public).

Каждый раз, когда пользователь запрашивает статический ресурс вида /assets/js/global.js, запрос будет послан на обработку контроллером AssetsController, если файла ещё нет в файловой системе. В противном случае, ресурс будет возвращён веб-сервером.

Это не самый лучший пример. Тем не менее, это отражает основную идею: разумная конфигурация веб-сервера с приложением может помочь оптимизировать время отклика многократно.

Узнать больше о конфигурировании веб-сервера и маршрутизации можно в соответствующих статьях Настройка веб-сервера и Маршрутизация.