Skip to content

Commit

Permalink
Merge pull request #112 from kirkbushell/release/11.0
Browse files Browse the repository at this point in the history
Eloquence 11.0
  • Loading branch information
kirkbushell authored Jan 31, 2024
2 parents ca0306a + 8ae46f4 commit 42ae6c8
Show file tree
Hide file tree
Showing 60 changed files with 1,185 additions and 967 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ composer.lock
.phpunit.result.cache
.DS_Store
.idea*
.php-cs-fixer.cache
321 changes: 165 additions & 156 deletions README.md

Large diffs are not rendered by default.

50 changes: 43 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
"name": "kirkbushell/eloquence",
"description": "A set of extensions adding additional functionality and consistency to Laravel's awesome Eloquent library.",
"keywords": [
"laravel",
"eloquent",
"aggregates",
"cache",
"camelcase",
"camel",
"case",
"count",
"eloquent",
"laravel",
"snake_case",
"snake"
"snake",
"sum"
],
"authors": [
{
Expand All @@ -21,13 +25,15 @@
"hashids/hashids": "^4.1",
"illuminate/database": "^10.0",
"illuminate/support": "^10.0",
"ramsey/uuid": "^4.7"
"hanneskod/classtools": "^0.1.0",
"symfony/finder": "^6.3"
},
"require-dev": {
"illuminate/events": "^10.0",
"mockery/mockery": "^1.4.4",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^9.5.10"
"phpunit/phpunit": "^9.5.10",
"friendsofphp/php-cs-fixer": "^3.48"
},
"autoload": {
"psr-4": {
Expand All @@ -36,8 +42,38 @@
}
},
"scripts": {
"test": "phpunit --colors=always"
"test": "phpunit --colors=always",
"post-autoload-dump": [
"@clear",
"@prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"serve": [
"@build",
"@php vendor/bin/testbench serve"
]
},
"minimum-stability": "dev",
"prefer-stable": true
"prefer-stable": true,
"extra": {
"laravel": {
"providers": [
"Eloquence\\EloquenceServiceProvider"
]
}
},
"autoload-dev": {
"psr-4": {
"Workbench\\App\\": "workbench/app/",
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
}
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
}
}
8 changes: 8 additions & 0 deletions config/eloquence.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

return [
'logging' => [
'enabled' => env('ELOQUENCE_LOGGING_ENABLED', false),
'driver' => env('ELOQUENCE_LOGGING_DRIVER', env('LOG_CHANNEL', 'stack')),
]
];
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@
<directory suffix="Test.php">./tests/Acceptance</directory>
</testsuite>
</testsuites>
<php>
<env name="API_KEY" value="fakeApiKey" force="true" />
</php>
</phpunit>
52 changes: 52 additions & 0 deletions src/Behaviours/CacheConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Eloquence\Behaviours;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;

class CacheConfig
{
public function __construct(readonly string $relationName, readonly string $aggregateField, readonly string $sourceField = 'id')
{
}

/**
* Returns the actual Relation object - such as BelongsTo. This method makes a call to the relationship
* method specified on the model object, and is used to infer data about the relationship.
*/
public function relation(Model $model): Relation
{
return $model->{$this->relationName}();
}

/**
* Returns the current related model.
*/
public function relatedModel(Model $model): Model
{
return $model->{$this->relationName};
}

/**
* Returns -a- related model object - this object is actually empty, and is found on the query builder, used to
* infer certain information abut the relationship that cannot be found on CacheConfig::relation.
*/
public function emptyRelatedModel(Model $model): Model
{
return $this->relation($model)->getModel();
}

/**
* Returns the related model class name.
*/
public function relatedModelClass($model): string
{
return get_class($this->emptyRelatedModel($model));
}

public function foreignKeyName(Model $model): string
{
return $this->relation($model)->getForeignKeyName();
}
}
173 changes: 69 additions & 104 deletions src/Behaviours/Cacheable.php
Original file line number Diff line number Diff line change
@@ -1,144 +1,109 @@
<?php

namespace Eloquence\Behaviours;

use Closure;
use Eloquence\Behaviours\SumCache\SummedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use Tests\Acceptance\Models\User;

/**
* The cacheable trait is concerned with the related models.
*/
trait Cacheable
{

/**
* Updates a table's record based on the query information provided in the $config variable.
* Allows cacheable to work with implementors and their unique relationship methods.
*
* @param array $config
* @param string $operation Whether to increase or decrease a value. Valid values: +/-
* @param int|float|double $amount
* @param string $foreignKey
* @return array
*/
public function updateCacheRecord(array $config, $operation, $amount, $foreignKey)
{
if (is_null($foreignKey)) {
return;
}

$config = $this->processConfig($config);

$sql = DB::table($config['table'])->where($config['key'], $foreignKey);

/*
* Increment for + operator
*/
if ($operation == '+') {
return $sql->increment($config['field'], $amount);
}

/*
* Decrement for - operator
*/
return $sql->decrement($config['field'], $amount);
}
abstract private function configuration(): array;

/**
* Rebuilds the cache for the records in question.
*
* @param array $config
* @param Model $model
* @param $command
* @param null $aggregateField
* @return mixed
* Helper method for easier use of the implementing classes.
*/
public function rebuildCacheRecord(array $config, Model $model, $command, $aggregateField = null)
public static function for(Model $model): self
{
$config = $this->processConfig($config);
$table = $this->getModelTable($model);

if (is_null($aggregateField)) {
$aggregateField = $config['foreignKey'];
} else {
$aggregateField = Str::snake($aggregateField);
}

$sql = DB::table($table)->select($config['foreignKey'])->groupBy($config['foreignKey']);

if (strtolower($command) == 'count') {
$aggregate = $sql->count($aggregateField);
} else if (strtolower($command) == 'sum') {
$aggregate = $sql->sum($aggregateField);
} else if (strtolower($command) == 'avg') {
$aggregate = $sql->avg($aggregateField);
} else {
$aggregate = null;
}
return new self($model);
}

return DB::table($config['table'])
->update([
$config['field'] => $aggregate
]);
public function reflect(string $attributeClass, \Closure $fn)
{
$reflect = new ReflectionClass($this->model);

// This behemoth cycles through all valid methods, and then gets only the attributes we care about,
// formatting it in a way that is usable by our various aggregate service classes.
return collect($reflect->getMethods())
->filter(fn (ReflectionMethod $method) => count($method->getAttributes($attributeClass)) > 0)
->flatten()
->map(function (ReflectionMethod $method) use ($attributeClass) {
return collect($method->getAttributes($attributeClass))->map(fn (\ReflectionAttribute $attribute) => [
'name' => $method->name,
'attribute' => $attribute->newInstance(),
])->toArray();
})
->flatten(1)
->mapWithKeys($fn)
->toArray();
}

/**
* Creates the key based on model properties and rules.
*
* @param string $model
* @param string $field
*
* @return string
* Applies the provided function using the relevant configuration to all configured relations. Configuration
* would be one of countedBy, summedBy, averagedBy.etc.
*/
protected function field($model, $field)
protected function apply(Closure $function): void
{
$class = strtolower(class_basename($model));
$field = $class . '_' . $field;

return $field;
foreach ($this->configuration() as $key => $value) {
$function($this->config($key, $value));
}
}

/**
* Process configuration parameters to check key names, fix snake casing, etc..
* Updates a table's record based on the query information provided in the $config variable.
*
* @param array $config
* @return array
* @param string $operation Whether to increase or decrease a value. Valid values: +/-
*/
protected function processConfig(array $config)
protected function updateCacheRecord(Model $model, CacheConfig $config, string $operation, int $amount): void
{
return [
'model' => $config['model'],
'table' => $this->getModelTable($config['model']),
'field' => Str::snake($config['field']),
'key' => Str::snake($this->key($config['key'])),
'foreignKey' => Str::snake($this->key($config['foreignKey'])),
];
$this->updateCacheValue($model, $config, $amount);
}

/**
* Returns the true key for a given field.
* It's a bit hard to read what's going on in this method, so let's elaborate.
*
* @param string $field
* @return mixed
* 1. Get the foreign key of the model that needs to be queried.
* 2. Get the aggregate value for all records with that foreign key.
* 3. Update the related model wth the relevant aggregate value.
*/
protected function key($field)
public function rebuildCacheRecord(CacheConfig $config, Model $model, $command): void
{
if (method_exists($this->model, 'getTrueKey')) {
return $this->model->getTrueKey($field);
}

return $field;
$foreignKey = $config->foreignKeyName($model);
$related = $config->emptyRelatedModel($model);

$updateSql = sprintf(
'UPDATE %s SET %s = COALESCE((SELECT %s(%s) FROM %s WHERE %s = %s.%s), 0)',
$related->getTable(),
$config->aggregateField,
$command,
$config->sourceField,
$model->getTable(),
$foreignKey,
$related->getTable(),
$related->getKeyName()
);

DB::update($updateSql);
}

/**
* Returns the table for a given model. Model can be an Eloquent model object, or a full namespaced
* class string.
*
* @param string|Model $model
* @return mixed
* Update the cache value for the model.
*/
protected function getModelTable($model)
protected function updateCacheValue(Model $model, CacheConfig $config, int $amount): void
{
if (!is_object($model)) {
$model = new $model;
}

return DB::getTablePrefix().$model->getTable();
$model->{$config->aggregateField} = $model->{$config->aggregateField} + $amount;
$model->save();
}

}
Loading

0 comments on commit 42ae6c8

Please sign in to comment.