tobento/app-slugging

1.0.1 2024-09-11 09:28 UTC

This package is auto-updated.

Last update: 2024-12-11 10:03:50 UTC


README

Slugging support for the app using the Slugifier Service.

Table of Contents

Getting Started

Add the latest version of the app slugging project running this command.

composer require tobento/app-slugging

Requirements

  • PHP 8.0 or greater

Documentation

App

Check out the App Skeleton if you are using the skeleton.

You may also check out the App to learn more about the app in general.

Slugging Boot

The slugging boot does the following:

  • installs and loads slugging config file
  • implements slugifier interfaces
use Tobento\App\AppFactory;
use Tobento\App\Slugging\SlugRepositoryInterface;
use Tobento\Service\Slugifier\SlugifierFactoryInterface;
use Tobento\Service\Slugifier\SlugifierInterface;
use Tobento\Service\Slugifier\SlugifiersInterface;
use Tobento\Service\Slugifier\SlugsInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots
$app->boot(\Tobento\App\Slugging\Boot\Slugging::class);
$app->booting();

// Implemented interfaces:
$slugifierFactory = $app->get(SlugifierFactoryInterface::class);
$slugifier = $app->get(SlugifierInterface::class);
$slugifiers = $app->get(SlugifiersInterface::class);
$slugs = $app->get(SlugsInterface::class);
$slugRepository = $app->get(SlugRepositoryInterface::class);

// Run the app
$app->run();

Slugging Config

The configuration for the slugging is located in the app/config/slugging.php file at the default App Skeleton config location where you can specify the slugifiers for your application and more.

Generating Slugs

To generate slugs use the slugifier interfaces:

use Tobento\Service\Slugifier\SlugifierInterface;
use Tobento\Service\Slugifier\SlugifiersInterface;

class SomeService
{
    public function __construct(
        protected SlugifierInterface $slugifier,
        protected SlugifiersInterface $slugifiers,
    ) {}
    
    private function slugify()
    {
        // using the default slugifier:
        $slug = $this->slugifier->slugify(string: 'Lorem Ipsum!', locale: 'de');
        
        // using a custom slugifier:
        $slug = $this->slugifiers->get('custom')->slugify('Lorem Ipsum!');
    }
}

You may check out the Slugifier Service to learn more about it.

Adding Slugs

You may add slugs to prevent dublicate slugs or for routing purposes such as using the Slug Matches on routes.

From Config

You may add slugs using resources directly in the Slugging Config.

Using The App

Sometimes, it may be useful to add slugs using resources within the app:

use Tobento\Service\Slugifier\Resource\ArrayResource;
use Tobento\Service\Slugifier\SlugsInterface;

// Adding slugs resources only if requested:
$app->on(SlugsInterface::class, static function(SlugsInterface $slugs): void {
    $slugs->addResource(new ArrayResource(
        slugs: ['login'],
    ));
});

Repository Resource

With the RepositoryResource class you can add any repository implementing the RepositoryInterface as a resource.

use Tobento\App\Slugging\Resource\RepositoryResource;
use Tobento\Service\Repository\RepositoryInterface;
use Tobento\Service\Slugifier\SlugsInterface;

// Adding slugs resources only if requested:
$app->on(SlugsInterface::class, static function(SlugsInterface $slugs, BlogRepositoryInterface $blogRepo): void {
    $slugs->addResource(new RepositoryResource(
        repository: $blogRepo,
        priority: 100, // higher priority will be first.
        
        resourceKey: 'blog', // or null
        // or using a closure:
        resourceKey: static function (null|object $blog): null|string {
            return $blog->resourceKey();
        },
        
        resourceId: static function (object $blog): null|string|int {
            return $blog->id();
        },
        // or null if none:
        resourceId: null,
        
        // you may customize the where query parameters:
        whereParameters: static function (string $slug, string $locale): array {
            return $locale === ''
                ? ['slug' => $slug] // locale independent (default)
                : ['slug' => $slug, 'locale' => $locale]; // locale dependent
            
            // JSON SYNTAX:
            return ['slug->'.$locale => $slug]; // locale dependent
        },
    ));
});

Slug Repository

By default, the slug repository is added to the slugs in the Slugging Config whereby preventing dublicated slugs.

The advantage using the slug repository is that there will be just one query while generating slugs or when using the Slug Matches if it is the only added slug resource.

Saving Slugs

Use the saveSlug method to save a slug:

use Tobento\App\Slugging\SlugRepositoryInterface;
use Tobento\Service\Slugifier\Slug;

$slugRepository = $app->get(SlugRepositoryInterface::class);

$savedSlug = $slugRepository->saveSlug(new Slug(
    slug: 'lorem-ipsum',
    locale: 'en',
    resourceKey: 'blog', // null|string
    resourceId: 125, // null|int|string
));

Deleting Slugs

Use the deleteSlug method to delete a slug:

use Tobento\App\Slugging\SlugRepositoryInterface;
use Tobento\Service\Slugifier\Slug;

$slugRepository = $app->get(SlugRepositoryInterface::class);

$deletedSlug = $slugRepository->deleteSlug(new Slug(
    slug: 'lorem-ipsum',
    locale: 'en',
));

Routing

First, you will need to install the App Http.

Slug Matches

You may use the SlugMatches class to have mutliple routes with a slug only uri matching different controllers based on the resouceKey parameter.

use Tobento\App\Slugging\Routing\SlugMatches;

$app->route(
    method: 'GET',
    uri: '{slug}',
    //handler: [BlogController::class, 'show'],
    handler: function (string $slug) {
        return $createdResponse;
    },
)->matches(new SlugMatches(resourceKey: 'blog'));

$app->route(
    method: 'GET',
    uri: '{slug}',
    //handler: [ProductController::class, 'show'],
    handler: function (string $slug) {
        return $createdResponse;
    },
)->matches(new SlugMatches(resourceKey: 'product'));

Using Locale

You may use the withLocale parameter to define the name of the uri locale parameter. Once defined, slugs will be matched locale dependent.

use Tobento\App\Slugging\Routing\SlugMatches;

$app->route(
    method: 'GET',
    uri: '{?locale}/{slug}',
    handler: function (string $slug) {
        return $createdResponse;
    },
)
->locales(['de', 'en'])
->localeOmit('en')
->matches(new SlugMatches(
    resourceKey: 'blog',
    withLocale: 'locale',
));

Using The Resource Id

You may use the withUriId parameter to define the name of the parameter passed to the handler whereby the resource id from the slug entity $slug->resourceId() will be passed.

use Tobento\App\Slugging\Routing\SlugMatches;

$app->route(
    method: 'GET',
    uri: '{slug}',
    handler: function (int|string $id) {
        return $createdResponse;
    },
)->matches(new SlugMatches(
    resourceKey: 'blog',
    withUriId: 'id',
));

Custom Slug Uri

You may use the uriSlugName parameter to change the uri name of the slug.

use Tobento\App\Slugging\Routing\SlugMatches;

$app->route(
    method: 'GET',
    uri: '{alias}',
    handler: function (string $alias) {
        return $createdResponse;
    },
)->matches(new SlugMatches(
    resourceKey: 'blog',
    uriSlugName: 'alias',
));

Unique Slug Validation Rule

Requirements

It requires the App Validation:

composer require tobento/app-validation

Do not forget to boot the validator:

use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Validation\Boot\Validator::class);
$app->boot(\Tobento\App\Slugging\Boot\Slugging::class);

// Run the app
$app->run();

Unique Slug Rule

use Tobento\App\Slugging\Validation\UniqueSlugRule;

$validation = $validator->validate(
    data: [
        'slug' => 'login',
        'slug.de' => 'anmelden',
        'slug.en' => 'login',
    ],
    rules: [
        'slug' => [
            new UniqueSlugRule(
                locale: 'en',
                // you may specify a custom error message:
                errorMessage: 'Custom error message',
            ),
        ],
        'slug.de' => [
            new UniqueSlugRule(), // locale is automatically determined as 'de'.
        ],
        'slug.en' => [
            new UniqueSlugRule(), // locale is automatically determined as 'en'.
        ],
    ]
);

Skip validation

You may use the skipValidation parameter in order to skip validation under certain conditions:

use Tobento\App\Slugging\Validation\UniqueSlugRule;

$validation = $validator->validate(
    data: [
        'slug.en' => 'login',
    ],
    rules: [
        'slug.en' => [
            // skips validation:
            new UniqueSlugRule(skipValidation: true),

            // does not skip validation:
            new UniqueSlugRule(skipValidation: false),

            // skips validation:
            new UniqueSlugRule(skipValidation: fn (mixed $value): bool => $value === 'foo'),
        ],
    ]
);

Credits