🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,54 @@
name: run-tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.4, 8.3, 8.2, 8.1]
laravel: [12.*, 11.*, 10.*]
stability: [prefer-stable]
include:
- laravel: 12.*
testbench: 10.*
- laravel: 11.*
testbench: 9.*
- laravel: 10.*
testbench: 8.*
exclude:
- laravel: 11.*
php: 8.1
- laravel: 12.*
php: 8.1
- laravel: 12.*
php: 8.2
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
coverage: none
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 AnourValar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,30 @@
# Serialization for Eloquent's QueryBuilder
Supports: Laravel 6 - Laravel 12
## Installation
```bash
composer require anourvalar/eloquent-serialize
```
## Usage
### Serialize
```php
$package = \EloquentSerialize::serialize(
\App\User::query()
->with('userPhones')
->where('id', '>', '10')
->limit(20)
);
```
### Unserialize
```php
$builder = \EloquentSerialize::unserialize($package);
foreach ($builder->get() as $item) {
// ...
}
```
@@ -0,0 +1,33 @@
{
"name": "anourvalar/eloquent-serialize",
"description": "Laravel Query Builder (Eloquent) serialization",
"keywords": ["laravel", "query", "builder", "querybuilder", "anourvalar", "serialize", "serialization", "eloquent", "serializable", "job", "queue", "copy"],
"homepage": "https://github.com/AnourValar/eloquent-serialize",
"license": "MIT",
"require": {
"php": "^7.4|^8.0",
"laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5|^10.5|^11.0",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
"laravel/legacy-factories": "^1.1",
"phpstan/phpstan": "^2.0",
"friendsofphp/php-cs-fixer": "^3.26",
"squizlabs/php_codesniffer": "^3.7",
"psalm/plugin-laravel": "^2.8|^3.0"
},
"autoload": {
"psr-4": {"AnourValar\\EloquentSerialize\\": "src/"}
},
"autoload-dev": {
"psr-4": {"AnourValar\\EloquentSerialize\\Tests\\": "tests/"}
},
"extra": {
"laravel": {
"aliases": {
"EloquentSerialize": "AnourValar\\EloquentSerialize\\Facades\\EloquentSerializeFacade"
}
}
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
>
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="EloquentSerialize">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
@@ -0,0 +1,20 @@
<?php
namespace AnourValar\EloquentSerialize\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static string serialize(\Illuminate\Database\Eloquent\Builder $builder)
* @method static \Illuminate\Database\Eloquent\Builder unserialize(mixed $package)
*/
class EloquentSerializeFacade extends Facade
{
/**
* @return string
*/
protected static function getFacadeAccessor()
{
return \AnourValar\EloquentSerialize\Service::class;
}
}
@@ -0,0 +1,147 @@
<?php
namespace AnourValar\EloquentSerialize\Grammars;
trait EloquentBuilderGrammar
{
/**
* Serialize state for \Illuminate\Database\Eloquent\Builder
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return array
*/
protected function packEloquentBuilder(\Illuminate\Database\Eloquent\Builder $builder): array
{
return [
'with' => $this->getEagers($builder), // preloaded ("eager") relations
'removed_scopes' => $builder->removedScopes(), // global scopes
'casts' => $builder->getModel()->getCasts(),
];
}
/**
* Unserialize state for \Illuminate\Database\Eloquent\Builder
*
* @param array $data
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
protected function unpackEloquentBuilder(array $data, \Illuminate\Database\Eloquent\Builder &$builder): void
{
// Preloaded ("eager") relations
$this->setEagers($builder, $data['with']);
// Global scopes
if ($data['removed_scopes']) {
$builder->withoutGlobalScopes($data['removed_scopes']);
}
// Casts
if (method_exists($builder->getModel(), 'mergeCasts')) { // old versions support
$builder->getModel()->mergeCasts($data['casts']);
}
}
/**
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return array
*/
private function getEagers(\Illuminate\Database\Eloquent\Builder $builder): array
{
$result = [];
foreach ($builder->getEagerLoads() as $name => $value) {
$relation = $builder;
foreach (explode('.', $name) as $part) {
$relation = $relation->getRelation($part); // get a relation without "constraints"
}
$referenceRelation = clone $relation;
$value($relation); // apply closure
$result[$name] = [
'query' => $this->packQueryBuilder($relation->getQuery()->getQuery()),
'eloquent' => $this->packEloquentBuilder($relation->getQuery()),
'extra' => $relation->exportExtraParametersForSerialize(),
];
$relation->getQuery()->getModel()->newInstance()->with($name)->getEagerLoads()[$name]($referenceRelation);
$this->cleanStaticConstraints($result[$name]['query'], $this->packQueryBuilder($referenceRelation->getQuery()->getQuery()));
}
return $result;
}
/**
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param array $eagers
* @return void
*/
private function setEagers(\Illuminate\Database\Eloquent\Builder $builder, array $eagers): void
{
foreach ($eagers as &$value) {
$value = function ($query) use ($value) {
if (isset($value['extra'])) {
$query->importExtraParametersForSerialize($value['extra']);
}
// Input argument may be different depends on context
while (! ($query instanceof \Illuminate\Database\Eloquent\Builder)) {
$query = $query->getQuery();
}
if (isset($value['eloquent'])) {
$this->unpackEloquentBuilder($value['eloquent'], $query);
}
// Input argument may be different depends on context
while (! ($query instanceof \Illuminate\Database\Query\Builder)) {
$query = $query->getQuery();
}
$this->unpackQueryBuilder(isset($value['query']) ? $value['query'] : $value, $query);
};
}
unset($value);
$builder->setEagerLoads($eagers);
}
/**
* @param array $packedQueryBuilder
* @param array $packedReferenceQueryBuilder
* @return void
*/
private function cleanStaticConstraints(array &$packedQueryBuilder, array $packedReferenceQueryBuilder): void
{
$properties = [
'aggregate', 'columns', 'distinct', 'wheres', 'groups', 'havings', 'orders', 'limit', 'offset', 'unions',
'unionLimit', 'unionOffset', 'unionOrders', 'joins', 'groupLimit',
];
foreach ($properties as $property) {
if (! is_array($packedQueryBuilder[$property] ?? null)) {
continue;
}
foreach ($packedQueryBuilder[$property] as $key => $item) {
if (in_array($item, (array) ($packedReferenceQueryBuilder[$property] ?? null), true)) {
unset($packedQueryBuilder[$property][$key]);
}
}
}
foreach ($packedQueryBuilder['bindings'] as $binding => $data) {
if (! is_array($data)) {
continue; // just in case ;)
}
foreach ($data as $key => $value) {
if (
isset($packedReferenceQueryBuilder['bindings'][$binding][$key])
&& $packedReferenceQueryBuilder['bindings'][$binding][$key] === $value
) {
unset($packedQueryBuilder['bindings'][$binding][$key]);
}
}
}
}
}
@@ -0,0 +1,158 @@
<?php
namespace AnourValar\EloquentSerialize\Grammars;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
trait ModelGrammar
{
/**
* Pack
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return \AnourValar\EloquentSerialize\Package
*/
protected function pack(\Illuminate\Database\Eloquent\Builder $builder): \AnourValar\EloquentSerialize\Package
{
$this->setup();
// Global scopes handle
$builder = $builder->applyScopes();
$builder->withoutGlobalScopes(array_keys($builder->getAllGlobalScopes()));
return new \AnourValar\EloquentSerialize\Package([
'model' => get_class($builder->getModel()),
'connection' => $builder->getModel()->getConnectionName(),
'eloquent' => $this->packEloquentBuilder($builder),
'query' => $this->packQueryBuilder($builder->getQuery()),
]);
}
/**
* Unpack
*
* @param \AnourValar\EloquentSerialize\Package $package
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function unpack(\AnourValar\EloquentSerialize\Package $package): \Illuminate\Database\Eloquent\Builder
{
$this->setup();
$builder = $package->get('model');
$builder = $builder::on($package->get('connection'));
$this->unpackEloquentBuilder($package->get('eloquent'), $builder);
$this->unpackQueryBuilder($package->get('query'), $builder->getQuery());
return $builder;
}
/**
* init
*
* @return void
*/
private function setup(): void
{
$serializeMorphableEager = fn ($value) => $this->serializeMorphableEager($value);
$unserializeMorphableEager = fn ($value) => $this->unserializeMorphableEager($value);
\Illuminate\Database\Eloquent\Builder::macro('getAllGlobalScopes', function () {
return $this->scopes;
});
\Illuminate\Database\Eloquent\Relations\Relation::macro('importExtraParametersForSerialize', function (array $params) use ($unserializeMorphableEager) {
foreach ($params as $key => $value) {
if ($key == 'morphableEagerLoads' || $key == 'morphableEagerLoadCounts') {
$value = $unserializeMorphableEager($value);
}
$this->$key = $value;
}
});
\Illuminate\Database\Eloquent\Relations\Relation::macro('exportExtraParametersForSerialize', function () use ($serializeMorphableEager) {
if ($this instanceof \Illuminate\Database\Eloquent\Relations\MorphTo) {
return [
'morphableEagerLoads' => $serializeMorphableEager($this->morphableEagerLoads),
'morphableEagerLoadCounts' => $serializeMorphableEager($this->morphableEagerLoadCounts),
'morphableConstraints' => $this->morphableConstraints,
];
}
if (
$this instanceof HasOneOrMany
&& in_array(\Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations::class, class_uses(HasOneOrMany::class)) // @TODO: >= 11.22
) {
return [
'inverseRelationship' => $this->inverseRelationship,
];
}
return null;
});
}
/**
* @param array $value
* @return array
* @psalm-suppress UndefinedClass
*/
private function serializeMorphableEager(array $value): array
{
foreach ($value as $class => &$items) {
foreach ($items as $relation => &$item) {
if (! is_callable($item)) {
continue;
}
if (! method_exists($class, $relation)) {
throw new \RuntimeException("Serialization error. Does relation '{$relation}' exists in the model '{$class}' ?");
}
// ALT: $obj->cleanStaticConstraints($item['builder'], $obj->packQueryBuilder((new $class)->getQuery()->getQuery()));
$eloquentBuilder = (new $class())->$relation()->getModel()->newQuery();
$item($eloquentBuilder);
$item = [
'eloquent' => $this->packEloquentBuilder($eloquentBuilder),
'builder' => $this->packQueryBuilder($eloquentBuilder->getQuery()),
];
}
unset($item);
}
unset($items);
return $value;
}
/**
* @param array $value
* @return array
*/
private function unserializeMorphableEager(array $value): array
{
foreach ($value as &$items) {
foreach ($items as &$item) {
if (! is_array($item)) {
continue;
}
$item = function ($query) use ($item) {
if ($query instanceof \Illuminate\Database\Eloquent\Builder) {
$this->unpackEloquentBuilder($item['eloquent'], $query);
$this->unpackQueryBuilder($item['builder'], $query->getQuery());
} else {
$eloquent = $query->getQuery();
$this->unpackEloquentBuilder($item['eloquent'], $eloquent);
$this->unpackQueryBuilder($item['builder'], $query->getBaseQuery());
}
};
}
unset($item);
}
unset($items);
return $value;
}
}
@@ -0,0 +1,191 @@
<?php
namespace AnourValar\EloquentSerialize\Grammars;
trait QueryBuilderGrammar
{
/**
* Serialize state for \Illuminate\Database\Query\Builder
*
* @param \Illuminate\Database\Query\Builder $builder
* @return array
*/
protected function packQueryBuilder(\Illuminate\Database\Query\Builder $builder): array
{
return array_filter([
'bindings' => $builder->bindings,
'aggregate' => $builder->aggregate,
'columns' => $builder->columns,
'distinct' => $builder->distinct,
'from' => $builder->from,
'wheres' => $this->packWheres($builder->wheres),
'groups' => $builder->groups,
'havings' => $this->packWheres($builder->havings),
'groupLimit' => $builder->groupLimit ?? null,
'orders' => $builder->orders,
'limit' => $builder->limit,
'offset' => $builder->offset,
'unions' => $this->packUnions($builder->unions),
'unionLimit' => $builder->unionLimit,
'unionOffset' => $builder->unionOffset,
'unionOrders' => $builder->unionOrders,
'lock' => $builder->lock,
'joins' => $this->packJoins($builder->joins), // must be the last
], fn ($item) => isset($item));
}
/**
* @param array $data
* @param \Illuminate\Database\Query\Builder $builder
* @return \Illuminate\Database\Query\Builder
*/
protected function unpackQueryBuilder(array $data, \Illuminate\Database\Query\Builder $builder): \Illuminate\Database\Query\Builder
{
foreach ($data as $key => $value) {
if (in_array($key, ['wheres', 'havings'])) {
$value = $this->unpackWheres($value, $builder);
}
if ($key == 'unions') {
$value = $this->unpackUnions($value);
}
if ($key == 'joins') {
$value = $this->unpackJoins($value, $builder);
}
if (is_array($builder->$key) && is_array($value)) {
$builder->$key = array_merge_recursive($builder->$key, $value);
} else {
$builder->$key = $value;
}
}
return $builder;
}
/**
* @param mixed $wheres
* @return mixed
*/
private function packWheres($wheres)
{
if (is_null($wheres)) {
return $wheres;
}
foreach ($wheres as &$item) {
if (isset($item['query'])) {
$item['query'] = $this->packQueryBuilder($item['query']);
}
}
unset($item);
return $wheres;
}
/**
* @param mixed $unions
* @return mixed
*/
private function packUnions($unions)
{
if (! is_array($unions)) {
return $unions;
}
foreach ($unions as &$item) {
if (isset($item['query'])) {
$item['query'] = $this->pack($item['query']);
}
}
unset($item);
return $unions;
}
/**
* @param mixed $joins
* @return mixed
*/
private function packJoins($joins)
{
if (! is_array($joins)) {
return $joins;
}
foreach ($joins as &$item) {
$item = array_replace(
['type' => $item->type, 'table' => $item->table],
$this->packQueryBuilder($item)
);
}
unset($item);
return $joins;
}
/**
* @param mixed $wheres
* @param \Illuminate\Database\Query\Builder $builder
* @return mixed
*/
private function unpackWheres($wheres, \Illuminate\Database\Query\Builder $builder)
{
if (is_null($wheres)) {
return $wheres;
}
foreach ($wheres as &$item) {
if (isset($item['query'])) {
$item['query'] = $this->unpackQueryBuilder($item['query'], $builder->newQuery());
}
}
unset($item);
return $wheres;
}
/**
* @param mixed $unions
* @return mixed
*/
private function unpackUnions($unions)
{
if (! is_array($unions)) {
return $unions;
}
foreach ($unions as &$item) {
if (isset($item['query'])) {
$item['query'] = $this->unpack($item['query']);
}
}
unset($item);
return $unions;
}
/**
* @param mixed $joins
* @param \Illuminate\Database\Query\Builder $builder
* @return mixed
*/
private function unpackJoins($joins, \Illuminate\Database\Query\Builder $builder)
{
if (! is_array($joins)) {
return $joins;
}
foreach ($joins as &$item) {
$parentQuery = new \Illuminate\Database\Query\JoinClause($builder, $item['type'], $item['table']);
unset($item['type'], $item['table']);
$item = $this->unpackQueryBuilder($item, $parentQuery);
}
unset($item);
return $joins;
}
}
@@ -0,0 +1,33 @@
<?php
namespace AnourValar\EloquentSerialize;
class Package
{
/**
* $var array
*/
private $data;
/**
* @param array $data
* @return void
*/
public function __construct(array $data)
{
$this->data = $data;
}
/**
* @param string|null $key
* @return mixed
*/
public function get(?string $key = null)
{
if (is_null($key)) {
return $this->data;
}
return ($this->data[$key] ?? null);
}
}
@@ -0,0 +1,60 @@
<?php
namespace AnourValar\EloquentSerialize;
use Illuminate\Database\Eloquent\Relations\Relation;
class Service
{
use \AnourValar\EloquentSerialize\Grammars\ModelGrammar;
use \AnourValar\EloquentSerialize\Grammars\EloquentBuilderGrammar;
use \AnourValar\EloquentSerialize\Grammars\QueryBuilderGrammar;
/**
* Pack
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $builder
* @return string
* @throws \RuntimeException
*/
public function serialize(\Illuminate\Database\Eloquent\Builder|Relation $builder): string
{
if (
$builder instanceof \Illuminate\Database\Eloquent\Relations\HasOne
|| $builder instanceof \Illuminate\Database\Eloquent\Relations\HasMany
|| $builder instanceof \Illuminate\Database\Eloquent\Relations\BelongsTo // as well as MorphTo
|| $builder instanceof \Illuminate\Database\Eloquent\Relations\MorphOne
) {
$builder = $builder->getQuery(); // chaperone/inverse is not supported!
}
if ($builder instanceof Relation) {
throw new \RuntimeException(get_class($builder) . ' cannot be packed.');
}
$package = $this->pack($builder);
return serialize($package); // important!
}
/**
* Unpack
*
* @param mixed $package
* @throws \LogicException
* @return \Illuminate\Database\Eloquent\Builder
*/
public function unserialize($package): \Illuminate\Database\Eloquent\Builder
{
// Prepare data
if (is_string($package)) {
$package = unserialize($package);
}
if (! ($package instanceof Package)) {
throw new \LogicException('Incorrect argument.');
}
// Unpack
return $this->unpack($package);
}
}