Pull to refresh

Создание пакета для Laravel

Reading time 11 min
Views 28K
Привет Хабр!

Фреймворк Laravel быстро набирает популярность и уже обрел большую армию фанатов. В этой статье я опишу разработку простого пакета для Laravel, а так же публикацию созданного нами пакета на сайте packagist.org для того, чтобы добавлять наш пакет в проект одной строчкой в composer.json.

Итак, о чем все это


Пакеты (packages) — основной способ добавления нового функционала в Laravel, так говорит нам официальная документация. Мы создадим пакет специально для Laravel, это будет простой блог со списком записей и страницей для просмотра статьи. Мы не ставим целью написать превосходный, универсальный блог с кучей возможностей для кастомизации, мы лишь рассмотрим все тонкости создания пакета, как независимого расширения, которое поможет в будущем избежать дублирования кода в своих проектах, при решении рутинных задач.

В итоге мы получим это.



Чему мы научимся


  • Как создавать пакет, расширяющий функциональность проекта;
  • Познакомимся с базовой структурой внутри пакета;
  • Разберемся как добавлять свои маршруты;
  • Узнаем где хранить шаблоны;
  • Покажем composer какие классы нам нужно добавить в автозагрузку;
  • Создадим миграции и заполним таблицы начальными данными;
  • Сделаем доступными внешние ресурсы (css, js — файлы) пакета.


Необходимые инструменты


Я веду разработку под MacOS, использую IDE PhpStorm, а сам сайт будет крутиться на официальном Vagrant боксе — Homestead.
На хабре уже были полезные статьи о том, как настроить Vagrant образ, а так же о том, как установить сам Laravel.

Что же в конечном итоге будет представлять наш пакет


У него будет два роута:

  • Страница со списком постов;
  • Отдельная страница поста.


Несколько шаблонов:

  • Общий layout блога;
  • Страница со списком записей;
  • Краткий вид записи в списке постов;
  • Полный вид записи.


Одна модель Post с полями:

  • Заголовок поста
  • Уникальная ссылка поста
  • Текст записи
  • Время создания
  • Время обновления


А так же вычисляемое поле cut, где из текста записи берутся первые 120 символов.
В дополнение ко всему, мы создадим класс контроллера, который будет группировать логику отображения страниц, дополнительный файл составителей, для привязки необходимых данных к нашим шаблонам.
В заключении создадим файл миграции таблицы с постами, так же seed класс с некоторым количеством начальных данных.
Опубликуем стили пакета в public-папке приложения.

Приступаем к работе


Итак, Laravel установлен, любимая IDE открыта и мы можем начинать.



Пропишем в файле app/config/workbench.php параметры name и email, в дальнейшем они будут использованы в composer.json для вашего пакета.
Прописав эти параметры выполните в консоли:

php artisan workbench vendor/package --resources

Где vendor имя поставщика услуг, package это имя создаваемого вами пакета.

К примеру мой логин на github — cherryoff, а создаваемый пакет я хочу назвать nbblog, соответственно я должен выполнить команду:

php artisan workbench --resources cherryoff/nbblog




Где флаг resourses говорит что необходимо так же создать специфичные для Laravel папки: migrations, views, config и тд
Если все прошло гладко, то в корне проекта вы увидите:



Чтобы Laravel при запуске приложения автоматически подгружал наш пакет нам необходимо в файл app/config/app.php в массив providers добавить строчку:

'Cherryoff\Nbblog\NbblogServiceProvider',


Имена классов поставщика услуг следуют схеме [Package]ServiceProvider, но перед этим указывается полный namespace класса

Структура пакета


В папке src мы можем видеть знакомую структуру папок, названия которых говорят сами за себя.
Заметим, что в папке src/Cherryoff/Nbblog/ хранится класс нашего поставщика услуг, а так же туда стоит класть все вспомогательные классы нашего пакета.
Посмотрим на класс NbblogServiceProvider. Метод register будет вызван, как только пакет был зарегистрирован, а метод boot вызывается каждый раз перед обработкой запроса.

Сосредоточимся на создании блога


Создадим в папке src, нашего пакета файл routes.php с двумя роутами:

Route::get('/blog/', array(
            'as' => 'posts_list',
            'uses' => 'Cherryoff\Nbblog\NbblogController@showList'
        ));

Route::get('/blog/{link}', array(
            'as' => 'post',
            'uses' => 'Cherryoff\Nbblog\NbblogController@showPost'
        ))->where('link', '[A-Za-z-_]+');


Где в качестве контроллера указываем полный путь до нашего еще не созданного контроллера.
Далее в папке controllers создадим контроллер NbblogController со следующим содержимым:


<?php namespace Cherryoff\Nbblog;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL;

class NbblogController extends Controller {

        public function showList()
        {
            return 'Posts list';
        }

        public function showPost($link)
        {
            return 'Get post:'.$link;
        }

    }


Отлично, теперь у нас прописаны пути и контроллер должен отвечать нам простыми сообщениями. Но если мы перейдем по ссылке sandbox.local/blog, то получим исключение о том что страница не найдена. Все дело в том, что приложение не знает о том, что у нашего пакета есть свои пути, и чтобы исправить это подключим файл routes.php в конце метода boot класса NbblogServiceProvider:

include __DIR__.'/../../routes.php';


Но даже после этого ничего не заработает и мы словим ошибку:

Class Cherryoff\Nbblog\NbblogController does not exist

Это признак того, что composer в нашем пакете ничего не знает о только что добавленном контроллере. Чтобы показать composer где искать необходимые файлы, добавим в composer.json (нашего пакета!!!) в секцию classmap строчку «src/controllers», после чего выполним
composer dump-autoload 

Теперь, если мы перейдем по адресу:
sandbox.local/blog
то увидим строчку:
Post list
Ура! Это значит что пути работают, а наш контроллер подцепился!
Далее, создадим папку models и файл Post.php в ней. Листинг файла представлен ниже:

<?php namespace Cherryoff\Nbblog;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Config;

/**
 * Модель записи в блоге
 */

class Post extends Model {

    protected $table = 'posts';
    //Добавляем в выдачу вычисляемое поле
    protected $appends = array('cut');
    //Делаем поля доступными для автозаполнения
    protected $fillable = array('header', 'link', 'article');

//Некоторые правила валидиции
    public static $rules = array(
        'header' => 'required|max:256',
        'link' => 'required|between:2,32|unique',
        'article' => 'required'
    );

    public function getCutAttribute(){
        return Str::limit($this->attributes['article'], 120);
    }

}


Теперь необходимо добавить папку models в секцию автозагрузки composer.json (нужно добавить выше строчки "src/controllers") нашего проекта и выполнить composer dump-autoload так же, как мы это делали в случае с контроллером.

Подробнее о Eloquent моделях можно почитать на оф сайте Laravel

Строкой в консоли создадим миграцию для нашего пакета:

 php artisan migrate:make create_nbblog_posts_table --bench="cherryoff/nbblog"


В папке src/migrations/ появился класс только что созданной миграции. В его методе up пропишем:

Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('link', 32);
            $table->string('header', 256);
            $table->text('article');
            $table->timestamps();
            $table->softDeletes();
        });


В методе down:

Schema::dropIfExists('posts');


Таким образом в методе up мы создаем таблицу с необходимыми полями для нашей модели, а в методе down удаляем ее, если она существует

Итак, пришло время выполнить нашу миграцию:

php artisan migrate --bench="cherryoff/nbblog"


Если вы правильно настроили соединение с базой данных, то в консоли увидите что то вроде:

Migration table created successfully.
Migrated: 2014_10_23_115450_create_nbblog_posts_table


Теперь заполним только что созданную таблицу начальными данными. Для этого в папке src создадим папку seeds с файлом NbblogSeeder.php со следующим содержимым:

NbblogSeeder.php
<?php namespace Cherryoff\Nbblog;

use Illuminate\Database\Seeder;

class NbblogSeeder extends Seeder {

    public function run()
    {
        $posts = [
            [
                'header'=>'Header post number one',
                'link'=>'one',
                'article'=>'
                    In condimentum facilisis porta. Sed nec diam eu diam mattis viverra. Nulla fringilla, orci ac euismod semper, magna diam
                    porttitor mauris, quis sollicitudin sapien justo in libero. Vestibulum mollis mauris enim. Morbi euismod magna ac lorem
                    rutrum elementum. Donec viverra auctor lobortis. Pellentesque eu est a nulla placerat dignissim. Morbi a enim in magna
                    semper bibendum. Etiam scelerisque, nunc ac egestas consequat, odio nibh euismod nulla, eget auctor orci nibh vel nisi.
                    Aliquam erat volutpat. Mauris vel neque sit amet nunc gravida congue sed sit amet purus. Quisque lacus quam, egestas ac
                    tincidunt a, lacinia vel velit. Aenean facilisis nulla vitae urna tincidunt congue sed ut dui. Morbi malesuada nulla nec
                    purus convallis consequat. Vivamus id mollis quam. Morbi ac commodo nulla. In condimentum orci id nisl volutpat bibendum.
                    Quisque commodo hendrerit lorem quis egestas. Maecenas quis tortor arcu. Vivamus rutrum nunc non neque consectetur quis placerat
                    neque lobortis. Nam vestibulum, arcu sodales feugiat consectetur, nisl orci bibendum elit, eu euismod magna sapien ut nibh.
                    Donec semper quam scelerisque tortor dictum gravida. In hac habitasse platea dictumst. Nam pulvinar, odio sed rhoncus suscipit,
                    sem diam ultrices mauris, eu consequat purus metus eu velit. Proin metus odio, aliquam eget molestie nec, gravida ut sapien.
                    Phasellus quis est sed turpis sollicitudin venenatis sed eu odio. Praesent eget neque eu eros interdum malesuada non vel leo.
                    Sed fringilla porta ligula egestas tincidunt. Nullam risus magna, ornare vitae varius eget, scelerisque.
                ',
            ],
            [
                'header'=>'Very important news',
                'link'=>'news',
                'article'=>'
                    Donec congue lacinia dui, a porttitor lectus condimentum laoreet. Nunc eu ullamcorper orci. Quisque eget odio ac
                    lectus vestibulum faucibus eget in metus. In pellentesque faucibus vestibulum. Nulla at nulla justo, eget luctus tortor.
                    Nulla facilisi. Duis aliquet egestas purus in blandit. Curabitur vulputate, ligula lacinia scelerisque tempor, lacus lacus
                    ornare ante, ac egestas est urna sit amet arcu. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
                    himenaeos. Sed molestie augue sit amet leo consequat posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
                    posuere cubilia Curae; Proin vel ante a orci tempus eleifend ut et magna. Lorem ipsum dolor sit amet, consectetur adipiscing
                    elit. Vivamus luctus urna sed urna ultricies ac tempor dui sagittis. In condimentum facilisis porta. Sed nec diam eu diam mattis
                    viverra. Nulla fringilla, orci ac euismod semper, magna diam porttitor mauris, quis sollicitudin sapien.
                ',
            ],
        ];

        foreach ($posts as $post){
            Post::create($post);
        }
    }

}   



Добавим папку seeds в секцию автозагрузки composer.json и снова выполним
composer dump-autoload

Теперь загрузим начальные данные командой:

 php artisan db:seed --class="\Cherryoff\Nbblog\NbblogSeeder"


Если мы все сделали правильно, то все завершилось без ошибок.

Создадим шаблоны видов для нашего блога, разместив их в папке src/views:

layout.blade.php
<!DOCTYPE html>
<html>
<head>
    <link href='http://fonts.googleapis.com/css?family=Lora&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
    <title>
        @yield('title')
    </title>
</head>
<body>
<div class="content">
    <header>
        <h1>My simple blog</h1>
        <small>Just blog package for Laravel</small>
    </header>
    <nav>
        <ul>
            <li><a href="/">Main page</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
    </nav>
    @yield('content')
</div>
</body>
</html>


list.blade.php

@section('title')
    List
@stop

@section('content')
    <small>Number of posts in the blog: {{$count}}</small>
    <ul class="posts-list">
        @forelse($posts as $post)
            @include('nbblog::preview')
        @empty
            <li><h3>No records</h3></li>
        @endforelse
    </ul>
@stop



preview.blade.php
<li>
    <span><small>{{$post->created_at}}</small></span>
    <h2><a href="/blog/{{$post->link}}">{{$post->header}}</a></h2>
    <p>{{$post->cut}}</p>
</li>


post.blade.php
@section('title')
    {{$header}}
@stop

@section('content')
    <div class="post-block">
        <span><small>{{$created_at}}</small></span>
        <h2>{{$header}}</h2>
        <p>
            {{$article}}
        </p>
    </div>
@stop


Хотелось бы отметить, что обращаться к видам пакета нужно так:
имя_пакета:: путь_к_файлу, что в нашем случае:

nbblog::preview

Итак, с шаблонами закончено, теперь можно приступить к наполнению их данными. Для этого создадим файл viewComposers.php прямо в папке src. (Мы можем создать этот файл в любом месте нашего пакета, главное не забыть его подключить).

viewComposers.php
<?php
    /**
     * Не забываем использовать имя своего пакета перед названием вида
     */
    View::composer(array('nbblog::list', 'nbblog::post'), function($view){
        $view->with('uri', 'blog');
    });

    View::composer('nbblog::list', function ($view) {
        $view->with('count', \Cherryoff\Nbblog\Post::count())->with('posts', \Cherryoff\Nbblog\Post::all());
    });



Мы только что привязали переменную uri к шаблону списка постов и шаблону поста (в дальнейшем, когда мы будем получать эту переменную из настроек, нам будет удобнее передавать ее в виды в одном месте) и вместе с шаблоном списка постов мы отдаем сразу все записи.

Теперь необходимо подключить созданный файл в классе нашего поставщика услуг (src/Cherryoff/Nbblog/NbblogServiceProvider.php), так же, как мы это делали с файлом route.php.

        /**
         * Подключаем собственный viewComposers
         */
        include __DIR__.'/../../viewComposers.php';


Меняем класс нашего контролера, чтобы он выглядел как то так:

class NbblogController extends Controller {

        public function __construct(){
            $this->layout = View::make('nbblog::layout');
        }

        public function showList()
        {
            $this->layout->content = View::make('nbblog::list');
        }

        public function showPost($link)
        {
            $post = Post::where('link', '=', $link)->firstOrFail();
            $this->layout->content = View::make('nbblog::post', $post);
        }
    }



Все, теперь наш блог готов. Можем оценить это по адресу: sandbox.local/blog
Список записей выводится, посты просматриваются.



Но выглядит он как то не очень и нам хотелось бы это исправить. Для этого в папке public нашего пакета создадим папку css и добавим туда файл main.css со следующим содержимым:

main.css
html, body {
    font-family: 'Lora', "Times New Roman", serif;
    padding: 0;
    color: #383840;
    background-color: #F2F2F2;
}

a {
    color: #676673;
}

a:hover {
    color: #383840;
}

span, small {
    color: #B8B8BF;
}

.content {
    width: 600px;
    margin: 0 auto;
}

header {
    text-align: center;
}

header h1 {
    margin-bottom: 5px;
}

nav {
    width: auto;
    margin: 0 auto;
    text-align: center;
}

nav ul {
    padding: 0;
    margin-top: 10px;
    margin-bottom: 20px;
}

nav li {
    list-style: none;
    display: inline-block;
    padding: 10px 5px;
    margin: 0 20px;
}

.posts-list {
    padding: 0;
}

.posts-list li {
    list-style: none;
    border-bottom: 1px dotted #B8B8BF;
    padding-bottom: 10px;
    margin-top: 20px;
    margin-bottom: 60px;
}

.posts-list li > span, .post-block > span {
    width: 100%;
    text-align: center;
    display: inline-block;
    border-bottom: 1px dotted #B8B8BF;
    height: 10px;
}

.posts-list li > span small, .post-block > span small {
    background-color: #F2F2F2;
    padding: 10px;
}

.posts-list h2 {
    text-align: center;

}

.posts-list a {
    text-decoration: none;
    padding: 10px 20px;
}



Опубликуем внешние ресурсы нашего пакета командой:

php artisan asset:publish --bench="cherryoff/nbblog"


Возможно мы этого не заметили, но в папке public нашего приложения появился файл main.css, который расположился в папке packages/cherryoff/nbblog/css/
Как вы успели догадаться, так laravel делает со всеми внешними ресурсами пакетов. А значит, что это соглашение об именовании внешних ресурсов поможет нам обратится к этому файлу из нашего шаблона.

В шаблоне layout.blade.php перед тегом title вставим строчку:

{{ HTML::style('/packages/cherryoff/nbblog/css/main.css') }}


Это путь до нашего файла в папке public нашего приложения.

Если мы обновим страницу, то увидим простейший блог, который мы только что создали.



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

Тут вы можете посмотреть проект на github.
Живое демо.
Или просто добавить зависимость в composer.json вашего Laravel-проекта строку: «cherryoff/nbblog»: «dev-master»

Итог


Итак, мы научились создавать пакеты для Laravel, увидели из чего состоит пакет, теперь ничто не составляет труда форкнуть пакет стороннего разработчика и адаптировать его под свои нужды. Мы увидели, как происходит маршрутизация в пакете, узнали как показать composer где брать файлы для автозагрузки, узнали как делаются миграции и сиды в рамках пакета, а так же опубликовали свои ресурсы в public папке нашего приложения.

Статья получилась, чуть больше, чем я планировал, поэтому немного о том, что я расскажу в следующих статьях:
  • Покажу как вынести настройки пакета и предоставить возможность менять их пользователю не трогая сами файлы пакета;
  • Развернем git репозиторий в папке пакета;
  • И запишем все это дело на Github;
  • При желании опубликуем только что созданный пакет в packagist;
  • Включим автообновление пакета в packagist при обновлении на github;
  • Накатим наш пакет на чистый Laravel.


Если и это окажется интересно, то далее мы расширим функциональность нашего пакета:
  • Добавим к постам теги;
  • Добавим интерфейс добавления/редактирования/удаления постов;
  • Сделаем наш пакет более кастомизируемым.


Надеюсь не утомил, чистого кода!

Ссылки, используемые при написании статьи


Cайт Laravel;
Перевод официальной документации;
Вводит в курс дела;
Рассказывает о Homestead.
Tags:
Hubs:
+15
Comments 3
Comments Comments 3

Articles