Každý blog by měl mít vlastní RSS feed. V tomto článku si úplně od začátku vytvoříme vlastní RSS feed a ukážeme si, že pomocí balíčku od Spatie to lze provést velmi jednoduše a rychle. Pro naše účely využijeme balíček spatie/laravel-feed a celý projekt budeme stavět na Laravelu 8.

Příprava

Jako první si vytvoříme úplně nový Laravel projekt pomocí příkazu:

laravel new rss-project

Dále si nainstalujeme composer balíček spatie/laravel-feed a zkopírujeme si jeho konfigurační soubory do našeho projektu:

composer require spatie/laravel-feed
php artisan vendor:publish --provider="Spatie\Feed\FeedServiceProvider" --tag="config"

Modelování dat

Vytvoříme si pár testovacích dat pro model. Náš model budou články třeba na blog a RSS bude obsahovat kolekci těchto článků. Jako první si vytvoříme model společně s migrací a faktorkou pro vygenerování testovacích dat:

php artisan make:model Post -mf
Model created successfully.
Factory created successfully.
Created Migration: 2020_10_08_043318_create_posts_table

V migraci si definujeme velmi jednoduchou strukturu:

    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->foreignId('user_id');
            $table->timestamps();
        });
    }

Pro naše účely nám kromě ID a časových razítek stačí název článku, jeho obsah a autor. Autora článku si definujeme pomocí cizího klíče, kdy výchozí migrace čerstvého Laravel projektu již obsahují vytvoření tabulky User.

Dále si otevřeme soubor database/factories/PostFactory.php, který obsahuje definování našich testovacích dat pro seedování. Laravel 8 faktorky již generuje ve třídě, proto její obsah bude vypadat takto:

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title' => $this->faker->sentence,
            'content' => $this->faker->paragraph(),
            'user_id' => User::factory()
        ];
    }
}

V metodě definition() si nadefinujeme pomocí fakeru jednoduché dummy data. Budeme předpokládat, že každý článek napsal někdo jiný a tedy při každém vytvoření článku se vytvoří i jeho autor.

Dále si vytvoříme samostatný seeder, který bude využívat faktorku pro naseedování dat:

php artisan make:seeder PostsSeeder

Ve vytvořené třídě Database\Seeder\PostsSeeder si vytvoříme např. 50 nových článků a jejich autorů:

<?php

namespace Database\Seeders;

use App\Models\Post;
use Illuminate\Database\Seeder;

class PostsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Post::factory()->times(50)->create();
    }
}

Jako poslední pro vygenerování dat nám chybí zaregistrování tohoto seederu do hlavní třídy DatabaseSeeder:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(PostsSeeder::class);
    }
}

A nakonec spustíme migrace (předpokládám, že jste si již správně nastavili připojení k databázi). Společně s migracemi si necháme vygenerovat i data:

php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (61.27ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (40.83ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (37.86ms)
Migrating: 2020_10_08_043318_create_posts_table
Migrated:  2020_10_08_043318_create_posts_table (28.55ms)
Seeding: Database\Seeders\PostsSeeder
Seeded:  Database\Seeders\PostsSeeder (165.05ms)
Database seeding completed successfully.

Nastavení modelu

Abychom mohli využívat composer balíčku spatie/laravel-feed, musíme prvně implementovat interface Feedable v našem novém modelu Post. Díky tomuto rozhraní musíme implementovat i další dvě metody (ve skutečnosti pouze jednu, ale bez druhé to fungovat nebude):

  • toFeedItem() pro vyvoření jednotlivých položek do samotného RSS feedu
  • getFeedItems() - statická metoda pro získání všech RSS feed položek

Jako první si přidáme metodu toFeedItem():

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Feed\Feedable;
use Spatie\Feed\FeedItem;

class Post extends Model implements Feedable
{
    use HasFactory;

    public function toFeedItem(): FeedItem
    {
        return FeedItem::create()
            ->id($this->id)
            ->title($this->title)
            ->summary($this->content)
            ->updated($this->updated_at)
            ->link($this->link)
            ->author($this->user->name);
    }
}

Metoda je pomerně pochopitelná, nicméně stále nefunkční. Jako první je potřeba získat data pro autora. To uděláme jednoduše pomocí definování vztahu mezi článkem (tabulka posts) a uživatelem (tabulka users):

public function user()
{
   return $this->belongsTo(User::class);
}

Další atribut, který nám chybí vyřešit, je link. Ten aktuálně nikde neexistuje a musíme si jej v modelu sami vytvořit. Využijeme implicitního routování a automatického navázání modelu, takže se nám vygeneruje odkaz na článek:

public function getLinkAttribute()
{
   return route('posts.show', $this);
}

Jako poslední zbývá do modelu doplnit statickou metodu getFeedItems(), která vrací kolekci dat, které jsou jednotlivě převedeny do instance FeedItem:

public static function getFeedItems()
{
   return static::all();
}

Routy a controller

Jako poslední nám zbývá vytvoření controlleru a cest tak, aby nám route('posts.show', $this) fungovalo správně. Jako první si vygeneruje controller:

php artisan make:controller PostsController
Controller created successfully.

V nově vytvořeném PostController si vytvoříme novou metodu show(), která model kvůli zjednodušení vrací v JSON odpovědi:

<?php

namespace App\Http\Controllers;

use App\Models\Post;

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

Dále si potřebujeme upravit konfigurační soubor config/feed.php, který jsme si dříve vygenerovali. Nejdůležitější je zde si upravit cestu k položkám:

<?php

return [
    'feeds' => [
        'main' => [
            /*
             * Here you can specify which class and method will return
             * the items that should appear in the feed. For example:
             * 'App\Model@getAllFeedItems'
             *
             * You can also pass an argument to that method:
             * ['App\Model@getAllFeedItems', 'argument']
             */
            'items' => 'App\Models\Post@getFeedItems',

            /*
             * The feed will be available on this url.
             */
            'url' => '/posts.rss',

            'title' => 'All posts',
            'description' => 'The description of the feed.',
            'language' => 'en-US',

            /*
             * The view that will render the feed.
             */
            'view' => 'feed::atom',

            /*
             * The type to be used in the <link> tag
             */
            'type' => 'application/atom+xml',
        ],
    ],
];

A úplně poslední, co nám zbývá, je přidání RSS cesty na konec souboru routes/web:

use App\Http\Controllers\PostsController;

Route::get('/posts/{post}', [PostsController::class, 'show'])->name('posts.show');

Route::feeds();

Metoda Route::feeds() je makro, které definuje nezbytné cesty z naší config/feed.php konfigurace. Dále využíváme parametru {post}, který se automaticky naváže podle jeho primárního klíče na danou cestu.

A to je vše, náš RSS feed by měl být zcela dostupný na /posts.rss.

Máte dotazy, něco vám není jasné nebo mám někde chybu? Napište mi do komentářů!