Skip to content

Commit 536befd

Browse files
committed
BuilderExtensions POC
1 parent bb65800 commit 536befd

File tree

8 files changed

+410
-8
lines changed

8 files changed

+410
-8
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Attributes;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
8+
class BuilderExtension
9+
{
10+
public mixed $identifiers;
11+
12+
public function __construct(mixed ...$identifiers)
13+
{
14+
$this->identifiers = $identifiers;
15+
}
16+
}

Eloquent/Builder.php

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ class Builder implements BuilderContract
136136
'torawsql',
137137
];
138138

139+
protected array $extensions = [];
140+
139141
/**
140142
* Applied global scopes.
141143
*
@@ -203,6 +205,11 @@ public function withGlobalScope($identifier, $scope)
203205
return $this;
204206
}
205207

208+
public function addBuilderExtension($extension, $extensionType)
209+
{
210+
$this->extensions[$extension] = $extensionType;
211+
}
212+
206213
/**
207214
* Remove a registered global scope.
208215
*
@@ -1378,9 +1385,10 @@ protected function addUpdatedAtToUpsertColumns(array $update)
13781385
/**
13791386
* Delete records from the database.
13801387
*
1388+
* @param null $id
13811389
* @return mixed
13821390
*/
1383-
public function delete()
1391+
public function delete($id = null)
13841392
{
13851393
if (isset($this->onDelete)) {
13861394
return call_user_func($this->onDelete, $this);
@@ -1423,6 +1431,26 @@ public function hasNamedScope($scope)
14231431
return $this->model && $this->model->hasNamedScope($scope);
14241432
}
14251433

1434+
public function hasLocalBuilderExtension($builderExtension) : bool
1435+
{
1436+
return $this->model && $this->model->hasLocalBuilderExtension($builderExtension);
1437+
}
1438+
1439+
public function hasRegularBuilderExtension($builderExtension) : bool
1440+
{
1441+
$regularBuilderExtensions = array_filter($this->extensions, function ($extensionType, $extension){
1442+
return $extensionType == BuilderExtensionType::REGULAR;
1443+
}, ARRAY_FILTER_USE_BOTH);
1444+
1445+
foreach ($regularBuilderExtensions as $extension => $extensionType) {
1446+
if(str_contains(strtolower($extension), strtolower($builderExtension))) {
1447+
return true;
1448+
}
1449+
}
1450+
1451+
return false;
1452+
}
1453+
14261454
/**
14271455
* Call the given local model scopes.
14281456
*
@@ -1459,11 +1487,21 @@ public function scopes($scopes)
14591487
*/
14601488
public function applyScopes()
14611489
{
1462-
if (! $this->scopes) {
1463-
return $this;
1490+
$builder = clone $this;
1491+
1492+
$requiredBuilderExtensions = array_filter($this->extensions, function($extensionType, $extension) {
1493+
return $extensionType == BuilderExtensionType::REQUIRED;
1494+
}, ARRAY_FILTER_USE_BOTH);
1495+
1496+
if(count($requiredBuilderExtensions) > 0) {
1497+
foreach ($requiredBuilderExtensions as $extension => $extensionType) {
1498+
new $extension()->apply($builder);
1499+
}
14641500
}
14651501

1466-
$builder = clone $this;
1502+
if (! $this->scopes) {
1503+
return $builder;
1504+
}
14671505

14681506
foreach ($this->scopes as $identifier => $scope) {
14691507
if (! isset($builder->scopes[$identifier])) {
@@ -1533,6 +1571,31 @@ protected function callNamedScope($scope, array $parameters = [])
15331571
}, $parameters);
15341572
}
15351573

1574+
protected function callLocalBuilderExtension($builderExtension, array $parameters = []) : Builder
1575+
{
1576+
$this->model->$builderExtension($this, $parameters);
1577+
1578+
return $this;
1579+
}
1580+
1581+
protected function callRegularBuilderExtension($builderExtension, array $parameters = []) : Builder
1582+
{
1583+
$regularBuilderExtensions = array_filter($this->extensions, function ($extensionType, $extension) use($builderExtension) {
1584+
return $extensionType == BuilderExtensionType::REGULAR &&
1585+
str_contains(strtolower($extension), strtolower($builderExtension));
1586+
}, ARRAY_FILTER_USE_BOTH);
1587+
1588+
if(count($regularBuilderExtensions) == 0) {
1589+
throw new \LogicException('callRegularBuilderExtension method called without extension exist chack.');
1590+
}
1591+
1592+
$extension = array_key_first($regularBuilderExtensions);
1593+
1594+
new $extension()->apply($this);
1595+
1596+
return $this;
1597+
}
1598+
15361599
/**
15371600
* Nest where conditions by slicing them at the given where count.
15381601
*
@@ -2135,6 +2198,14 @@ public function __call($method, $parameters)
21352198
return $callable(...$parameters);
21362199
}
21372200

2201+
if($this->hasLocalBuilderExtension($method)) {
2202+
return $this->callLocalBuilderExtension($method, $parameters);
2203+
}
2204+
2205+
if($this->hasRegularBuilderExtension($method)) {
2206+
return $this->callRegularBuilderExtension($method, $parameters);
2207+
}
2208+
21382209
if ($this->hasNamedScope($method)) {
21392210
return $this->callNamedScope($method, $parameters);
21402211
}

Eloquent/BuilderExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
interface BuilderExtension
6+
{
7+
public function apply(Builder &$builder);
8+
}

Eloquent/BuilderExtensionType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
enum BuilderExtensionType : int
6+
{
7+
case REGULAR = 0;
8+
case REQUIRED = 1;
9+
10+
case LOCALE = 2;
11+
}

Eloquent/Model.php

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
1212
use Illuminate\Contracts\Support\Jsonable;
1313
use Illuminate\Database\ConnectionResolverInterface as Resolver;
14+
use Illuminate\Database\Eloquent\Attributes\BuilderExtension;
1415
use Illuminate\Database\Eloquent\Attributes\Scope as LocalScope;
1516
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
1617
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -25,6 +26,7 @@
2526
use JsonException;
2627
use JsonSerializable;
2728
use LogicException;
29+
use ReflectionClass;
2830
use ReflectionMethod;
2931
use Stringable;
3032

@@ -162,6 +164,8 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
162164
*/
163165
protected static $globalScopes = [];
164166

167+
protected static array $builderExtensions = [];
168+
165169
/**
166170
* The list of models classes that should not be affected with touch.
167171
*
@@ -300,6 +304,68 @@ protected static function booting()
300304
protected static function boot()
301305
{
302306
static::bootTraits();
307+
static::bootBuilderExtensions();
308+
}
309+
310+
protected static function bootBuilderExtensions()
311+
{
312+
static::$builderExtensions = [];
313+
$reflectionClass = new ReflectionClass(static::class);
314+
315+
$classBuilderExtensions = $reflectionClass->getAttributes(BuilderExtension::class);
316+
317+
foreach ($classBuilderExtensions as $classBuilderExtension) {
318+
$arguments = $classBuilderExtension->getArguments();
319+
$argumentsCount = count($arguments);
320+
321+
if($argumentsCount == 0){
322+
throw new LogicException('Builder Extension must have at least one argument');
323+
}
324+
325+
if($argumentsCount > 2) {
326+
throw new LogicException('Builder Extension has maximun two arguments');
327+
}
328+
329+
if($argumentsCount == 1){
330+
$argument = $arguments[0];
331+
332+
if(is_string($argument)){
333+
static::$builderExtensions[$argument] = BuilderExtensionType::REGULAR;
334+
} elseif(is_array($argument)){
335+
if(is_string(array_key_first($argument))){
336+
foreach($argument as $extension => $extensionType){
337+
static::$builderExtensions[$extension] = $extensionType;
338+
}
339+
} else {
340+
foreach($argument as $extension){
341+
static::$builderExtensions[$extension] = BuilderExtensionType::REGULAR;
342+
}
343+
}
344+
}
345+
}
346+
347+
if($argumentsCount == 2){
348+
static::$builderExtensions[$arguments[0]] = $arguments[1];
349+
}
350+
}
351+
352+
$methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
353+
354+
foreach ($methods as $method) {
355+
$attributes = $method->getAttributes(BuilderExtension::class);
356+
357+
if(count($attributes) > 1){
358+
throw new LogicException('a method can have only one BuilderExtension attribute');
359+
}
360+
361+
if(count($attributes) == 1 && count($attributes[0]->getArguments()) > 0){
362+
throw new LogicException('a method BuilderExtension attribute as no arguments');
363+
}
364+
365+
if(count($attributes) == 1) {
366+
static::$builderExtensions[$method->getName()] = BuilderExtensionType::LOCALE;
367+
}
368+
}
303369
}
304370

305371
/**
@@ -366,6 +432,8 @@ public static function clearBootedModels()
366432
static::$booted = [];
367433

368434
static::$globalScopes = [];
435+
436+
static::$builderExtensions = [];
369437
}
370438

371439
/**
@@ -1522,7 +1590,7 @@ public static function query()
15221590
*/
15231591
public function newQuery()
15241592
{
1525-
return $this->registerGlobalScopes($this->newQueryWithoutScopes());
1593+
return $this->registerBuilderExtensions($this->registerGlobalScopes($this->newQueryWithoutScopes()));
15261594
}
15271595

15281596
/**
@@ -1544,7 +1612,7 @@ public function newModelQuery()
15441612
*/
15451613
public function newQueryWithoutRelationships()
15461614
{
1547-
return $this->registerGlobalScopes($this->newModelQuery());
1615+
return $this->registerBuilderExtensions($this->registerGlobalScopes($this->newModelQuery()));
15481616
}
15491617

15501618
/**
@@ -1562,6 +1630,19 @@ public function registerGlobalScopes($builder)
15621630
return $builder;
15631631
}
15641632

1633+
public function registerBuilderExtensions(Builder $builder)
1634+
{
1635+
$builderExtensions = array_filter(static::$builderExtensions, function($builderExtensionType, $builderExtension) {
1636+
return in_array($builderExtensionType, [BuilderExtensionType::REGULAR, BuilderExtensionType::REQUIRED]);
1637+
}, ARRAY_FILTER_USE_BOTH);
1638+
1639+
foreach ($builderExtensions as $builderExtension => $builderExtensionType) {
1640+
$builder->addBuilderExtension($builderExtension, $builderExtensionType);
1641+
}
1642+
1643+
return $builder;
1644+
}
1645+
15651646
/**
15661647
* Get a new query builder that doesn't have any global scopes.
15671648
*
@@ -1633,6 +1714,11 @@ public function newPivot(self $parent, array $attributes, $table, $exists, $usin
16331714
: Pivot::fromAttributes($parent, $attributes, $table, $exists);
16341715
}
16351716

1717+
public Function hasBuilderExtension($name) : bool
1718+
{
1719+
return isset(static::$builderExtensions[$name]);
1720+
}
1721+
16361722
/**
16371723
* Determine if the model has a given scope.
16381724
*
@@ -1645,6 +1731,11 @@ public function hasNamedScope($scope)
16451731
static::isScopeMethodWithAttribute($scope);
16461732
}
16471733

1734+
public function hasLocalBuilderExtension($name) : bool
1735+
{
1736+
return isset(static::$builderExtensions[$name]) && static::$builderExtensions[$name] == BuilderExtensionType::LOCALE;
1737+
}
1738+
16481739
/**
16491740
* Apply the given named scope if possible.
16501741
*
@@ -1654,7 +1745,11 @@ public function hasNamedScope($scope)
16541745
*/
16551746
public function callNamedScope($scope, array $parameters = [])
16561747
{
1657-
if ($this->isScopeMethodWithAttribute($scope)) {
1748+
$localBuilderExtensions = array_filter(static::$builderExtensions, function($builderExtensionType, $builderExtension) {
1749+
return $builderExtensionType === BuilderExtensionType::LOCALE;
1750+
}, ARRAY_FILTER_USE_BOTH);
1751+
1752+
if ($this->isScopeMethodWithAttribute($scope) || in_array($scope, array_keys($localBuilderExtensions))) {
16581753
return $this->{$scope}(...$parameters);
16591754
}
16601755

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
}
1616
],
1717
"require": {
18-
"php": "^8.3",
18+
"php": "^8.4",
1919
"ext-pdo": "*",
2020
"brick/math": "^0.11|^0.12",
2121
"illuminate/collections": "^13.0",

0 commit comments

Comments
 (0)