PHPackages                             matteoc99/laravel-preference - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. matteoc99/laravel-preference

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

matteoc99/laravel-preference
============================

Laravel package that aims to store and manage user settings/preferences in a simple and scalable manner

v2.2.0(1y ago)446[2 issues](https://github.com/matteoc99/laravel-preference/issues)MITPHPPHP ^8.2CI passing

Since Mar 17Pushed 9mo ago1 watchersCompare

[ Source](https://github.com/matteoc99/laravel-preference)[ Packagist](https://packagist.org/packages/matteoc99/laravel-preference)[ RSS](/packages/matteoc99-laravel-preference/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (5)Versions (23)Used By (0)

Laravel User Preferences
========================

[](#laravel-user-preferences)

[![Latest Version on Packagist](https://camo.githubusercontent.com/b09d02d9089c745249a1835d08b02469eb0fa75cd256243f6739545fb34eba7b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d617474656f6339392f6c61726176656c2d707265666572656e63652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/matteoc99/laravel-preference)[![Total Downloads](https://camo.githubusercontent.com/3692aa5619143b9d4ebd8af7ceb1a56b84d570be9240359d117678f787d4a7fc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d617474656f6339392f6c61726176656c2d707265666572656e63652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/matteoc99/laravel-preference)[![Tests](https://github.com/matteoc99/laravel-preference/actions/workflows/tests.yml/badge.svg)](https://github.com/matteoc99/laravel-preference/actions/workflows/tests.yml)[![codecov](https://camo.githubusercontent.com/5c42bbe638aa977bd65ade1e348c6543056957711911a6a3f03cd8ee38fefec0/68747470733a2f2f636f6465636f762e696f2f6769746875622f6d617474656f6339392f6c61726176656c2d707265666572656e63652f67726170682f62616467652e7376673f746f6b656e3d4753313945324f525234)](https://codecov.io/github/matteoc99/laravel-preference)

This Laravel package aims to store and manage user settings/preferences in a simple and scalable manner.

Table of Contents
=================

[](#table-of-contents)

- [Features](#features)
    - [Roadmap](#roadmap)
- [Installation](#installation)
- [Usage](#usage)
    - [Concepts](#concepts)
    - [Define your preferences](#define-your-preferences)
    - [Create a Preference](#create-a-preference)
    - [Preference Building](#preference-building)
- [Working with preferences](#working-with-preferences)
    - [Examples](#examples)
- [Casting](#casting)
    - [Available Casts](#available-casts)
    - [Custom Caster](#custom-caster)
- [Rules](#rules)
    - [Available Rules](#available-rules)
    - [Custom Rules](#custom-rules)
- [Policies](#policies)
- [Routing](#routing)
    - [Anantomy](#anantomy)
    - [Example](#config-example)
    - [Actions](#actions)
    - [Middlewares](#middlewares)
- [Security](#security)
- [Upgrade from v1](#upgrade-from-v1)
- [Test](#test)
- [Contributing](#contributing)
- [Security Vulnerabilities](#security-vulnerabilities)
- [Credits](#credits)
- [License](#license)
- [Support target](#support-target)

Features
--------

[](#features)

- Type safe Casting
- Validation &amp; Authorization
- Extensible (Create your own Validation Rules and Casts)
- Enum support
- Custom Api routes
    - work with preferences from a GUI or in addition to backend functionalities

### Roadmap

[](#roadmap)

- Event System -&gt; [\#13](https://github.com/matteoc99/laravel-preference/issues/13)
- Api Response customization -&gt; [\#14](https://github.com/matteoc99/laravel-preference/issues/14)
- QoL Helpers functions
- Caching
- Blade Directives
- Additional suggestions are welcome. (check out [Contributing](#contributing))

Installation
------------

[](#installation)

You can install the package via composer:

```
composer require matteoc99/laravel-preference
```

Important

consider installing also `graham-campbell/security-core:^4.0` to take advantage of xss cleaning. see [Security](#security) for more information

### Configuration

[](#configuration)

You can publish the config file with:

```
php artisan vendor:publish --tag="laravel-preference-config"
```

```
  'db' => [
        'connection' => null, //string: the connection name to use
        'preferences_table_name'      => 'preferences',
        'user_preferences_table_name' => 'user_preferences',
    ],
    'xss_cleaning' => true, // clean user input for cross site scripting attacks
    'routes' => [
        'enabled'     => false, // set true to register routes, more on that later
        'middlewares' => [
            'auth', // general middleware
            'user'=> 'verified', // optional, scoped middleware
            'user.general'=> 'verified' // optional, scoped & grouped middleware
        ],
        'prefix' => 'preferences',
        'groups'      => [
            //enum class list of preferences
            'general'=>General::class
        ],
        'scopes'=> [
           // as many preferenceable models as you want
            'user' => \Illuminate\Auth\Authenticatable::class
        ]
    ]
```

Note

Consider changing the base table names before running the migrations, if needed

Run the migrations with:

```
php artisan migrate
```

Usage
-----

[](#usage)

### Concepts

[](#concepts)

Each preference has at least a name and a caster. Names are stored in one or more enums and are the unique identifier for that preference

For additional validation you can add you custom Rule object.

For additional security you can add Policies

### Define your preferences

[](#define-your-preferences)

Organize them in one or more **string backed** enum.

Note

while it does not need to be string backed, its way more developer friendly. Especially when interacting over the APi

Each enum gets scoped and does not conflict with other enums with the same case

e.g.

```
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;

enum Preferences :string implements PreferenceGroup
{
    case LANGUAGE="language";
    case QUALITY="quality";
    case CONFIG="configuration";
}

enum General :string implements PreferenceGroup
{
    case LANGUAGE="language";
    case THEME="theme";
}
```

### Create a Preference

[](#create-a-preference)

#### single mode

[](#single-mode)

```
use Matteoc99\LaravelPreference\Enums\Cast;

public function up(): void
{
    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withDefaultValue("en")
        ->withRule(new InRule("en", "it", "de"))
        ->create();

    // Or
    PreferenceBuilder::init(Preferences::LANGUAGE)->create()
    // different enums with the same value do not conflict
    PreferenceBuilder::init(General::LANGUAGE)->create()

    // update
    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withRule(new InRule("en", "it", "de"))
        ->updateOrCreate()

    // or with casting
    PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
        ->withDefaultValue(Language::EN)
        ->create()

    // nullable support
    PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
        ->withDefaultValue(null)
        ->nullable()
        ->create()

}

public function down(): void
{
    PreferenceBuilder::delete(Preferences::LANGUAGE);
}
```

#### Bulk mode

[](#bulk-mode)

```
use Illuminate\Database\Migrations\Migration;use Matteoc99\LaravelPreference\Enums\Cast;use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;use Matteoc99\LaravelPreference\Rules\InRule;

return new class extends Migration {

    public function up(): void
    {

        PreferenceBuilder::initBulk($this->preferences(),
        true // nullable for the whole Bulk
        );
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        PreferenceBuilder::deleteBulk($this->preferences());
    }

    /**
     * Reverse the migrations.
     */
    public function preferences(): array
    {
       return [
            ['name' => Preferences::LANGUAGE, 'cast' => Cast::STRING, 'default_value' => 'en', 'rule' => new InRule("en", "it", "de")],
            ['name' => Preferences::THEME, 'cast' => Cast::STRING, 'default_value' => 'light'],
            ['name' => Preferences::CONFIGURATION, 'cast' => Cast::ARRAY],
            ['name' => Preferences::CONFIGURATION,
                'nullable' => true // or nullable for only one configuration
            ],
            // or an array of initialized single-mode builders
            PreferenceBuilder::init(Preferences::LANGUAGE)->withRule(new InRule("en", "it", "de")),
            PreferenceBuilder::init(Preferences::THEME)->withRule(new InRule("light", "dark"))
            //mixing both in one array is also possible
       ];
    }
};
```

Preference Building
-------------------

[](#preference-building)

Check all methods available to build a Preference### Available Methods

[](#available-methods)

This table includes a complete list of all features available, when building a preference.

Single-ModeBulk-Mode (array-keys)ConstrainsDescriptioninit(&gt;name&lt;,&gt;cast&lt;)`["name"=> >name >cast >nullable >default_value >description >policy >rule >allowed_valuesid == $this->id ;
    }
}
```

### Examples

[](#examples)

```
    $user->setPreference(Preferences::LANGUAGE,"de");
    $user->getPreference(Preferences::LANGUAGE); // 'de' as string

    $user->setPreference(Preferences::LANGUAGE,"fr");
    // ValidationException because of the rule: ->withRule(new InRule("en","it","de"))
    $user->setPreference(Preferences::LANGUAGE,2);
    // ValidationException because of the cast: Cast::STRING

    $user->removePreference(Preferences::LANGUAGE);
    $user->getPreference(Preferences::LANGUAGE); // 'en' as string

    // get all of type Preferences,
    $user->getPreferences(Preferences::class)
    // or of type general
    $user->getPreferences(General::class)
    //or all
    $user->getPreferences(): Collection of UserPreferences

    // removes all preferences set for tht user
    $user->removeAllPreferences();

```

Casting
-------

[](#casting)

Set the cast when creating a Preference

Note

a cast has 3 main jobs

- Basic validation
- Casting from and to the database
- Preparing Api Responses

#### Example:

[](#example)

```
PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
```

### Available Casts

[](#available-casts)

CastExplanationINTConverts and Validates a value to be an integer.FLOATConverts and Validates a value to be a floating-point number.STRINGConverts and Validates a value to be a string.BOOLConverts and Validates a value to be a boolean (regards non-empty as `true`).ARRAYConverts and Validates a value to be an array.BACKED\_ENUMEnsures the value is a BackedEnum type. Useful for enums with underlying values.ENUMEnsures the value is a UnitEnum type. Useful for enums without underlying values.OBJECTEnsures that the value is an object.NONENo casting is performed. Returns the value as-is.Date-CastsExplanationConverts a value using Carbon::parse, and always return a Carbon instance.
 Validation is Cast-SpecificDATEsets the time to be `00:00`.TIMEAlways uses the current date, setting only the timeDATETIMEwith both date and time(optionally).TIMESTAMPallows a string/int timestamp or a carbon instance### Custom Caster

[](#custom-caster)

Implement `CastableEnum`

Important

The custom caster needs to be a **string backed** enum

#### Example:

[](#example-1)

```
use Illuminate\Contracts\Validation\ValidationRule;
use Matteoc99\LaravelPreference\Contracts\CastableEnum;

enum MyCast: string implements CastableEnum
{
    case TIMEZONE = 'tz';

    public function validation(): ValidationRule|array|string|null
    {
        return match ($this) {
            self::TIMEZONE => 'timezone:all',
        };
    }

    public function castFromString(string $value): mixed
    {
        return match ($this) {
            self::TIMEZONE => $value,

        };
    }
    public function castToString(mixed $value): string
    {
        return match ($this) {
            self::TIMEZONE => (string)$value,
        };
    }

   public function castToDto(mixed $value): array
    {
        return ['value' => $value];
    }
}

 PreferenceBuilder::init(Preferences::TIMEZONE, MyCast::TIMEZONE)->create();
```

Rules
-----

[](#rules)

Additional validation, which can be way more complex than provided by the Cast

### Adding Rules

[](#adding-rules)

```
     PreferenceBuilder::init(General::VOLUME, Cast::INT)
        ->withRule(new LowerThanRule(5))
        ->updateOrCreate()

    PreferenceBuilder::initBulk([
        'name' => General::VOLUME,
        'cast' => Cast::INT
        'rule' => new LowerThanRule(5)
     ]);
```

### Available Rules

[](#available-rules)

RuleExampleDescriptionAndRule`new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))`Expects `n` ValidationRule, ensures all passOrRule`new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))`Expects `n` ValidationRule, ensures at least one passesLaravelRule`new LaravelRule("required|numeric")`Expects a string, containing a Laravel Validation RuleBetweenRule`new BetweenRule(2.4, 5.5)`For INT and FLOAT, check that the value is between min and maxInRule`new InRule("it","en","de")`Expects the value to be validated to be in that equal to one of the `n` paramsInstanceOfRule`new InstanceOfRule(Theme::class)`For non primitive casts, checks the instance of the value's class to validate. Tip: goes along well with the `OrRule`IsRule`new IsRule(Type::ITERABLE)`Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterableLowerThanRule`new LowerThanRule(5)`For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor### Custom Rules

[](#custom-rules)

Implement Laravel's `ValidationRule`

#### Example:

[](#example-2)

```
class MyRule implements ValidationRule
{

    protected array $data;

    public function __construct(...$data)
    {
        $this->data = $data;
    }

    public function message()
    {
        return sprintf("Wrong Timezone, one of: %s expected", implode(", ",$this->data));
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if(!Str::startsWith($value, $this->data)){
            $fail($this->message());
        }
    }
}

 PreferenceBuilder::init("timezone",MyCast::TIMEZONE)
            ->withRule(new MyRule("Europe","Asia"))
```

Policies
--------

[](#policies)

Each preference can have a Policy, should [isUserAuthorized](#isuserauthorized) not be enough for your usecase

### Creating policies

[](#creating-policies)

Implement `PreferencePolicy` and the 4 methods defined by the contract

parameterdescriptionAuthenticatable $userthe currently logged in user, if anyPreferenceableModel $modelthe model on which you are trying to modify the preferencePreferenceGroup $preferencethe preference enum in question### Adding policies

[](#adding-policies)

```
    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withPolicy(new MyPolicy())
        ->updateOrCreate()

    PreferenceBuilder::initBulk([
        'name' => Preferences::LANGUAGE,
        'policy' => new MyPolicy()
     ]);
```

Routing
-------

[](#routing)

Off by default, enable it in the config

Warning

**(Current) limitation**: it's not possible to set object casts via API

### Anantomy:

[](#anantomy)

'Scope': the `PreferenceableModel` Model
'Group': the `PreferenceGroup` enum

Routes then get transformed to:

ActionURIDescriptionGET/{prefix}/{scope}/{scope\_id}/{group}Retrieves all preferences for a given scope and group.GET/{prefix}/{scope}/{scope\_id}/{group}/{preference}Retrieves a specific preference within the scope and group.PUT/PATCH/{prefix}/{scope}/{scope\_id}/{group}/{preference}Updates a specific preference within the scope and group.DELETE/{prefix}/{scope}/{scope\_id}/{group}/{preference}Deletes a specific preference within the scope and group.which can all be accessed via the route name: {prefix}.{scope}.{group}.{index/get/update/delete}

#### URI Parameters

[](#uri-parameters)

`scope_id`: The unique identifier of the scope (e.g., a user's ID).
`preference`: The value of the specific preference enum (e.g., General::LANGUAGE-&gt;value).
`group`: A mapping of group names to their corresponding Enum classes. See config below
`scope`: A mapping of scope names to their corresponding Eloquent model. See config below

### Config Example:

[](#config-example)

```
 'routes' => [
        'enabled'     => true,
        'middlewares' => [
            'auth',
            'user'=> 'verified'
        ],
        'prefix' => 'custom_prefix',
        'groups'      => [
            'general'=>\Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General::class
            'video'=>\Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences::class
        ],
        'scopes'=> [
            'user' => \Matteoc99\LaravelPreference\Tests\TestSubjects\Models\User::class
        ]
    ]
```

will result in the following **route names**:

- custom\_prefix.user.general.index
- custom\_prefix.user.general.get
- custom\_prefix.user.general.update
- custom\_prefix.user.general.delete
- custom\_prefix.user.video.index
- custom\_prefix.user.video.get
- custom\_prefix.user.video.update
- custom\_prefix.user.video.delete

### Actions

[](#actions)

Note

Examples are with scope `user` and group `general`

#### INDEX

[](#index)

- Route Name: custom\_prefix.user.general.index
- Url params: `scope_id`
- Equivalent to: `$user->getPreferences(General::class)`
- Http method: GET
- Endpoint: '[https://your.domain/custom\_prefix/user/{scope\_id}/general](https://your.domain/custom_prefix/user/%7Bscope_id%7D/general)'

#### GET

[](#get)

- Route Name: custom\_prefix.user.general.get
- Url params: `scope_id`,`preference`
- Equivalent to: `$user->getPreference(General::{preference})`
- Http method: GET
- Endpoint: [https://your.domain/custom\_prefix/user/{scope\_id}/general/{preference}](https://your.domain/custom_prefix/user/%7Bscope_id%7D/general/%7Bpreference%7D)

#### UPDATE

[](#update)

- Route Name: custom\_prefix.user.general.update
- Url params: `scope_id`,`preference`
- Equivalent to: `$user->setPreference(General::{preference}, >valuevalue< }`

##### Enum Patching

[](#enum-patching)

When creating your enum preference, add `setAllowedClasses` containing the possible enums to reconstruct the value

Caution

if multiple cases are shared between enums, the first match is taken

then, when sending the value it varies:

- BackedEnum: send the value or the case
- UnitEnum: send the case

Example:

```
enum Theme
{
    case LIGHT;
    case DARK;
}
curl -X PATCH 'https://your.domain/custom_prefix/user/{scope_id}/general/{preference}' \
    -d '{"value": "DARK"}'
```

#### DELETE

[](#delete)

- Route Name: (custom\_prefix.user.general.delete)
- Url params: `scope_id`,`preference`
- Equivalent to: `$user->removePreference(General::{preference})`
- Http method: DELETE
- Endpoint: [https://your.domain/custom\_prefix/user/{scope\_id}/general/{preference}](https://your.domain/custom_prefix/user/%7Bscope_id%7D/general/%7Bpreference%7D)

### Middlewares

[](#middlewares)

set global or context specific middlewares in the config file

```
'middlewares' => [
'web', // required for Auth::user() and policies
'auth', //no key => general middleware which gets applied to all routes
'user'=> 'verified', //  scoped middleware only for user routes should you have other preferencable models
'user.general'=> 'verified' // scoped & grouped middleware only for a specific model + enum
],
```

Caution

**known Issues**: without the web middleware, you won't have access to the user via the Auth facade since it's set by the middleware. Looking into an alternative

Security
--------

[](#security)

XSS cleaning is only performed on user facing api calls. this can be disabled, if not required, with the config: `user_preference.xss_cleaning`

When setting preferences directly via `setPreference`this cleaning step is assumed to have already been performed, if necessary.

Consider installing [Security-Core](https://github.com/GrahamCampbell/Security-Core) to make use of this feature

Upgrade from v1
---------------

[](#upgrade-from-v1)

- implement `PreferenceGroup` in your Preference enums
- implement `PreferenceableModel` in you all Models that want to use preferences
- Switch from `HasValidation` to `ValidationRule`
- Signature changes on the trait: group got removed and name now requires a `PreferenceGroup`
- Builder: setting group got removed and name now expects a `PreferenceGroup` enum
- `DataRule` has been removed, add a constructor to get you own, tailored, params
- database serialization incompatibilities will require you to rerun your Preference migrations
    - [single mode](#single-mode): make sure to use `updateOrCreate`, e.g ` PreferenceBuilder::init(VideoPreferences::QUALITY)->updateOrCreate();`
    - [bulk mode](#bulk-mode): initBulk as usual, as it works with upsert

Test
----

[](#test)

`composer test`

`composer coverage`

#### Test the pipeline locally

[](#test-the-pipeline-locally)

check out [act](https://github.com/nektos/act)install it via [gh](https://nektosact.com/installation/gh.html)

then run: `composer pipeline`

Contributing
------------

[](#contributing)

See [Contributing](.github/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](SECURITY.md) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [matteoc99](https://github.com/mattoc99)
- [Joel Brown](https://stackoverflow.com/users/659653/joel-brown)for [this](https://stackoverflow.com/questions/10204902/database-design-for-user-settings/10228192#10228192) awesome starting point and initial inspiration

License
-------

[](#license)

The MIT License (MIT). Please check the [License File](LICENSE) for more information.

Support target
--------------

[](#support-target)

Package VersionLaravel VersionPHPMaintained1.x10&gt;=8.1❌2.0 &amp; 2.110 &amp; 11&gt;=8.1❌^2.210 &amp; 11 &amp; 12&gt;=8.2✅

###  Health Score

35

—

LowBetter than 79% of packages

Maintenance46

Moderate activity, may be stable

Popularity12

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 98.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~23 days

Recently: every ~92 days

Total

18

Last Release

377d ago

Major Versions

0.1.2-beta → 1.0.02024-03-22

1.1.0 → 2.0.0-alpha2024-04-01

PHP version history (2 changes)0.1.0-betaPHP ^8.1

v2.2.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/bf22f838f0ec5e823a67a06c50d0f629fbd52a441f512dac283d87850fdd1c7d?d=identicon)[matteoc99](/maintainers/matteoc99)

---

Top Contributors

[![matteoc99](https://avatars.githubusercontent.com/u/27855635?v=4)](https://github.com/matteoc99 "matteoc99 (80 commits)")[![GoodM4ven](https://avatars.githubusercontent.com/u/121377476?v=4)](https://github.com/GoodM4ven "GoodM4ven (1 commits)")

---

Tags

laravellaravel-10-packagelaravel-11-packagelaravel-packagepreferencessettingsuser-settings

###  Code Quality

TestsPHPUnit

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/matteoc99-laravel-preference/health.svg)

```
[![Health](https://phpackages.com/badges/matteoc99-laravel-preference/health.svg)](https://phpackages.com/packages/matteoc99-laravel-preference)
```

###  Alternatives

[wireui/wireui

TallStack components

1.8k1.3M16](/packages/wireui-wireui)[blair2004/nexopos

The Free Modern Point Of Sale System build with Laravel, TailwindCSS and Vue.js.

1.2k2.3k](/packages/blair2004-nexopos)[ramonrietdijk/livewire-tables

Dynamic tables for models with Laravel Livewire

21147.4k](/packages/ramonrietdijk-livewire-tables)[ronasit/laravel-helpers

Provided helpers function and some helper class.

1475.7k13](/packages/ronasit-laravel-helpers)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
