feat: add HasCompositePrimaryKey trait for composite primary key support
- Introduced a new trait `HasCompositePrimaryKey` to enable Eloquent models to handle composite primary keys, which are not supported natively by Eloquent. The trait requires models to implement a method to define the composite key columns and manages key retrieval for save and select queries. chore: add GrumPHP configuration for pre-commit checks - Added a `grumphp.yml` configuration file to enforce code style and static analysis checks before commits. This setup includes Laravel Pint for code style and Larastan for static analysis, ensuring code quality and consistency. chore: create PHPStan configuration file - Created a `phpstan.neon` file to configure PHPStan with Larastan for static analysis. The configuration specifies the paths to analyze and sets the analysis level to 5 for a balance between catching bugs and avoiding excessive strictness.
This commit is contained in:
@@ -43,6 +43,9 @@ jobs:
|
|||||||
- name: Vérification du style de code (Laravel Pint)
|
- name: Vérification du style de code (Laravel Pint)
|
||||||
run: vendor/bin/pint --test
|
run: vendor/bin/pint --test
|
||||||
|
|
||||||
|
- name: Analyse statique (Larastan / PHPStan)
|
||||||
|
run: vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=512M --no-progress
|
||||||
|
|
||||||
- name: Exécution des tests automatisés (PHPUnit)
|
- name: Exécution des tests automatisés (PHPUnit)
|
||||||
env:
|
env:
|
||||||
DB_CONNECTION: sqlite
|
DB_CONNECTION: sqlite
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apprend à un modèle Eloquent à gérer une clé primaire COMPOSITE
|
||||||
|
* (plusieurs colonnes), ce qu'Eloquent ne sait pas faire nativement.
|
||||||
|
*
|
||||||
|
* Eloquent suppose que `$primaryKey` est une seule colonne (chaîne). On garde
|
||||||
|
* donc `$primaryKey` sous forme de chaîne (contrat respecté) et on déclare les
|
||||||
|
* colonnes de la clé composite via compositeKeyColumns(). Les requêtes de
|
||||||
|
* mise à jour / suppression / sélection par clé filtrent alors sur CHAQUE
|
||||||
|
* colonne de la clé.
|
||||||
|
*
|
||||||
|
* Le modèle qui l'utilise doit :
|
||||||
|
* - déclarer `public $incrementing = false;`
|
||||||
|
* - implémenter compositeKeyColumns() en listant les colonnes de la clé.
|
||||||
|
*/
|
||||||
|
trait HasCompositePrimaryKey
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Colonnes qui composent la clé primaire composite.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
abstract protected function compositeKeyColumns(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valeur de la clé sous forme de tableau « colonne => valeur ».
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getKey()
|
||||||
|
{
|
||||||
|
$key = [];
|
||||||
|
|
||||||
|
foreach ($this->compositeKeyColumns() as $column) {
|
||||||
|
$key[$column] = $this->getAttribute($column);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cible chaque colonne de la clé composite lors d'un save / update.
|
||||||
|
*
|
||||||
|
* @param Builder<static> $query
|
||||||
|
* @return Builder<static>
|
||||||
|
*/
|
||||||
|
protected function setKeysForSaveQuery($query)
|
||||||
|
{
|
||||||
|
foreach ($this->compositeKeyColumns() as $column) {
|
||||||
|
$query->where($column, '=', $this->getKeyForSaveQuery($column));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valeur d'une colonne de la clé pour un save (valeur d'origine si dispo).
|
||||||
|
*
|
||||||
|
* @param string|null $column
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function getKeyForSaveQuery($column = null)
|
||||||
|
{
|
||||||
|
$column ??= $this->getKeyName();
|
||||||
|
|
||||||
|
return $this->original[$column] ?? $this->getAttribute($column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cible chaque colonne de la clé composite lors d'une sélection par clé.
|
||||||
|
*
|
||||||
|
* @param Builder<static> $query
|
||||||
|
* @return Builder<static>
|
||||||
|
*/
|
||||||
|
protected function setKeysForSelectQuery($query)
|
||||||
|
{
|
||||||
|
foreach ($this->compositeKeyColumns() as $column) {
|
||||||
|
$query->where($column, '=', $this->getKeyForSelectQuery($column));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valeur d'une colonne de la clé pour une sélection.
|
||||||
|
*
|
||||||
|
* @param string|null $column
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function getKeyForSelectQuery($column = null)
|
||||||
|
{
|
||||||
|
$column ??= $this->getKeyName();
|
||||||
|
|
||||||
|
return $this->original[$column] ?? $this->getAttribute($column);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,33 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\HasCompositePrimaryKey;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Utilisateur_Progression extends Model
|
class Utilisateur_Progression extends Model
|
||||||
{
|
{
|
||||||
|
use HasCompositePrimaryKey;
|
||||||
|
|
||||||
protected $table = 'utilisateur_progressions';
|
protected $table = 'utilisateur_progressions';
|
||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
|
||||||
protected $primaryKey = ['ID_Utilisateur', 'ID_Ressource'];
|
// Clé primaire composite (ID_Utilisateur + ID_Ressource), gérée par le
|
||||||
|
// trait via compositeKeyColumns(). Eloquent impose une chaîne pour
|
||||||
|
// $primaryKey : on y met la 1re colonne pour rester conforme au contrat.
|
||||||
|
protected $primaryKey = 'ID_Utilisateur';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colonnes composant la clé primaire composite.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
protected function compositeKeyColumns(): array
|
||||||
|
{
|
||||||
|
return ['ID_Utilisateur', 'ID_Ressource'];
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'ID_Utilisateur',
|
'ID_Utilisateur',
|
||||||
|
|||||||
+4
-1
@@ -12,11 +12,13 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
"larastan/larastan": "^3.10",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.24",
|
||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpro/grumphp": "^2.22",
|
||||||
"phpunit/phpunit": "^11.5.50"
|
"phpunit/phpunit": "^11.5.50"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -78,7 +80,8 @@
|
|||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"pestphp/pest-plugin": true,
|
"pestphp/pest-plugin": true,
|
||||||
"php-http/discovery": true
|
"php-http/discovery": true,
|
||||||
|
"phpro/grumphp": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
|
|||||||
Generated
+2575
-1
File diff suppressed because it is too large
Load Diff
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Configuration GrumPHP : vérifications automatiques avant chaque commit.
|
||||||
|
# À chaque `git commit`, GrumPHP lance ces tâches ; si l'une échoue,
|
||||||
|
# le commit est annulé. On évite ainsi de pousser du code non formaté
|
||||||
|
# ou contenant des erreurs détectables statiquement (= CI qui reste verte).
|
||||||
|
grumphp:
|
||||||
|
# On laisse le temps aux analyses (Larastan peut être un peu long).
|
||||||
|
process_timeout: 120
|
||||||
|
|
||||||
|
# On n'arrête pas au premier échec : on veut voir TOUS les problèmes
|
||||||
|
# (Pint ET PHPStan) en une seule fois.
|
||||||
|
stop_on_failure: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
# 1) Style de code (Laravel Pint) — mêmes règles qu'en CI.
|
||||||
|
shell:
|
||||||
|
scripts:
|
||||||
|
- ['-c', 'vendor/bin/pint --test']
|
||||||
|
|
||||||
|
# 2) Analyse statique (Larastan / PHPStan) — détecte les vrais bugs.
|
||||||
|
phpstan:
|
||||||
|
configuration: phpstan.neon
|
||||||
|
# false => on analyse les chemins définis dans phpstan.neon (app/)
|
||||||
|
# plutôt que seulement les fichiers modifiés : analyse plus fiable.
|
||||||
|
use_grumphp_paths: false
|
||||||
|
# Larastan charge tout le framework : 128M par défaut ne suffit pas.
|
||||||
|
memory_limit: '512M'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
includes:
|
||||||
|
- vendor/larastan/larastan/extension.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
paths:
|
||||||
|
- app/
|
||||||
|
|
||||||
|
# Niveau d'analyse statique (0 = souple, 9/max = très strict).
|
||||||
|
# On démarre à 5 : bon compromis pour attraper les vrais bugs
|
||||||
|
# (typage, null, méthodes inexistantes) sans bloquer sur des détails.
|
||||||
|
level: 5
|
||||||
Reference in New Issue
Block a user