PHP code example of andydefer / laravel-rattachments

1. Go to this page and download the library: Download andydefer/laravel-rattachments library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

andydefer / laravel-rattachments example snippets


// ❌ Une table par type de relation
// friendships table
// follows table
// blocks table
// tags table
// memberships table
// invitations table
// ...

// ❌ Ou des relations complexes avec des packages lourds
// Spatie\Permission, Laravel\Nova, etc.

// ✅ Une table pour toutes les relations
// Une seule table rattachments gère :
// - Amitiés
// - Abonnements
// - Blocages
// - Tags
// - Adhésions
// - Invitations
// - Et tout ce que vous voulez !

// Une relation est définie par :
// - Qui attache (rattachable)
// - Qui est attaché (target)
// - Le rôle de la relation
// - Des métadonnées optionnelles

$user1->attachTo($user2, FriendRole::FRIEND);  // Une amitié
$user1->attachTo($user2, BlockRole::BLOCKED);  // Un blocage
$doctor->attachTo($hospital, HospitalRole::DOCTOR); // Une affiliation
$post->attachTo($tag, TagRole::TAG); // Un tag

// Un médecin (rattachable) est rattaché à un hôpital (target)
// avec le rôle "doctor" et des métadonnées sur ses horaires

$attachment = $service->attach(
    $doctor,                    // rattachable
    $hospital,                  // target
    HospitalUserRole::DOCTOR,   // rôle
    [                           // métadonnées
        'consultation_days' => ['monday', 'wednesday', 'friday'],
        'consultation_hours' => '09:00-17:00',
    ]
);

// ✅ User → Hospital
$user->attachTo($hospital, Role::DOCTOR);

// ✅ Hospital → User
$hospital->attachTo($user, Role::PATIENT);

// ✅ User → User
$user1->attachTo($user2, FriendRole::FRIEND);

// ✅ Post → Tag
$post->attachTo($tag, TagRole::TAG);

// ✅ Doctor → Specialty
$doctor->attachTo($specialty, SpecialtyRole::PRIMARY);

final class User extends Model implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            Hospital::class => [
                HospitalUserRole::DOCTOR,
                HospitalUserRole::STAFF,
                HospitalUserRole::ADMIN,
            ],
            Pharmacy::class => [
                PharmacyUserRole::PHARMACIST,
                PharmacyUserRole::STAFF,
            ],
        ];
    }
}

public function uniqueTargets(): array
{
    return [
        // Un seul Hospital (n'importe quel rôle)
        Hospital::class => [],

        // Un seul CHIEF par Hospital
        Hospital::class => [Role::CHIEF],

        // Un seul BEST_FRIEND
        User::class => [FriendRole::BEST_FRIEND],

        // Une seule PRIMARY specialty
        Specialty::class => [Role::PRIMARY],
    ];
}

// ✅ OK - Premier CHIEF
$doctor->attachTo($hospital1, Role::CHIEF);

// ❌ ERREUR - Déjà CHIEF d'un hôpital
$doctor->attachTo($hospital2, Role::CHIEF);
// "Doctor already has a unique attachment to Hospital with role 'chief'"

// ✅ OK - Peut être DOCTOR dans plusieurs hôpitaux
$doctor->attachTo($hospital2, Role::DOCTOR);
$doctor->attachTo($hospital3, Role::DOCTOR);

public function disallowedTargets(): array
{
    return [
        Specialty::class => [],  // Bloque TOUS les rattachements
        Post::class => [PostUserRole::REVIEWER], // Bloque ce rôle
        Hospital::class => [HospitalUserRole::ADMIN], // Bloque ADMIN
    ];
}

// Rôles pour un médecin dans un hôpital
enum HospitalRole: string implements EnumerableInterface
{
    case DOCTOR = 'doctor';
    case CHIEF = 'chief';
    case RESIDENT = 'resident';

    public function getValue(): string
    {
        return $this->value;
    }
}

// Rôles pour un utilisateur dans un réseau social
enum FriendRole: string implements EnumerableInterface
{
    case FRIEND = 'friend';
    case BEST_FRIEND = 'best_friend';
    case ACQUAINTANCE = 'acquaintance';

    public function getValue(): string
    {
        return $this->value;
    }
}

// En base de données
$attachment = [
    'rattachable_type' => 'App\Models\User',
    'target_type' => 'App\Models\Hospital',
    'role' => 'doctor',
];

// Lecture - résolution automatique
$attachment->role; // HospitalUserRole::DOCTOR

// Autre contexte
$attachment = [
    'rattachable_type' => 'App\Models\Post',
    'target_type' => 'App\Models\Tag',
    'role' => 'primary',
];

// Lecture - résolution automatique
$attachment->role; // TagRole::PRIMARY

$role = $attachment->role;

if ($role instanceof UnknownRole) {
    Log::warning('Unknown role detected', [
        'attachment_id' => $attachment->id,
        'role_value' => $role->getValue(),
    ]);
}

use AndyDefer\LaravelRattachments\Enums\HookPosition;

enum HookPosition: string
{
    case RATTACHABLE = 'rattachable';
    case TARGET = 'target';
}

class User extends Model implements RattachmentInterface
{
    use HasRattachments;

    public function beforeAttach(
        Model $other,
        EnumerableInterface $role,
        array $metadata,
        HookPosition $position
    ): void {
        Log::info('Before attach', [
            'user_id' => $this->id,
            'other_type' => get_class($other),
            'other_id' => $other->getKey(),
            'role' => $role->getValue(),
            'position' => $position->value,
        ]);

        // Validation supplémentaire
        if ($position === HookPosition::RATTACHABLE && $role->getValue() === 'admin') {
            throw new \RuntimeException('Only administrators can attach with admin role');
        }
    }

    public function afterAttach(
        Model $other,
        EnumerableInterface $role,
        Model $attachment,
        HookPosition $position
    ): void {
        AuditLog::create([
            'user_id' => $this->id,
            'action' => 'attach',
            'target_type' => get_class($other),
            'target_id' => $other->getKey(),
            'role' => $role->getValue(),
            'attachment_id' => $attachment->id,
            'position' => $position->value,
        ]);

        // Invalider le cache
        Cache::forget("user_{$this->id}_attachments");

        // Envoyer une notification
        if ($position === HookPosition::TARGET) {
            $other->notify(new UserAttachedToYouNotification($this, $role));
        }
    }

    public function beforeDetach(
        Model $other,
        Model $attachment,
        HookPosition $position
    ): void {
        Log::info('Before detach', [
            'user_id' => $this->id,
            'attachment_id' => $attachment->id,
        ]);
    }

    public function afterDetach(
        Model $other,
        Model $attachment,
        HookPosition $position
    ): void {
        Cache::forget("user_{$this->id}_attachments");

        if ($position === HookPosition::RATTACHABLE) {
            $other->notify(new UserDetachedFromYouNotification($this));
        }
    }
}

use AndyDefer\LaravelRattachments\Services\RattachmentService;

class DoctorController extends Controller
{
    public function __construct(
        private readonly RattachmentService $rattachmentService
    ) {}
}

public function attachToHospital(Hospital $hospital)
{
    $doctor = auth()->user();

    $attachment = $this->rattachmentService->attach(
        $doctor,
        $hospital,
        HospitalUserRole::DOCTOR,
        [
            'consultation_days' => ['monday', 'wednesday', 'friday'],
            'consultation_hours' => '09:00-17:00',
        ]
    );

    return response()->json([
        'message' => 'Docteur rattaché à l\'hôpital',
        'role' => $attachment->role->getValue(),
        'metadata' => $attachment->metadata,
    ]);
}

// Rattacher plusieurs modèles à une même cible
$attachments = $this->rattachmentService->attachMultiple(
    collect([$doctor1, $doctor2, $doctor3]),
    $hospital,
    HospitalUserRole::DOCTOR,
    ['department' => 'Cardiology']
);

// Rattacher un modèle à plusieurs cibles
$attachments = $this->rattachmentService->attachToMultiple(
    $doctor,
    collect([$hospital1, $hospital2, $hospital3]),
    HospitalUserRole::DOCTOR
);

// Supprimer un rattachement spécifique
$this->rattachmentService->detach($doctor, $hospital);

// Supprimer plusieurs rattachements
$this->rattachmentService->detachMultiple(
    collect([$doctor1, $doctor2]),
    $hospital
);

// Supprimer tous les rattachements d'un modèle
$this->rattachmentService->detachAll($doctor);

// Vérifier si un modèle est rattaché à un autre
if ($this->rattachmentService->isAttached($doctor, $hospital)) {
    // ...
}

// Vérifier si un rôle existe pour une cible
if ($this->rattachmentService->hasRoleAttached($hospital, HospitalUserRole::DOCTOR)) {
    // L'hôpital a au moins un médecin
}

// Récupérer un attachement spécifique
$attachment = $this->rattachmentService->getAttachment($doctor, $hospital);

// Récupérer tous les modèles rattachés à une cible
$doctors = $this->rattachmentService->getRattachables($hospital);

// Récupérer les modèles rattachés avec un rôle spécifique
$doctors = $this->rattachmentService->getRattachablesByRole($hospital, HospitalUserRole::DOCTOR);

// Récupérer les modèles rattachés d'un type spécifique
$doctors = $this->rattachmentService->getRattachablesByType($hospital, Doctor::class);

// Récupérer les modèles rattachés d'un type et rôle spécifiques
$chiefs = $this->rattachmentService->getRattachablesByTypeAndRole($hospital, Doctor::class, HospitalRole::CHIEF);

// Récupérer les modèles rattachés d'un type avec plusieurs rôles
$staff = $this->rattachmentService->getRattachablesByTypeAndRoles($hospital, Doctor::class, [HospitalRole::DOCTOR, HospitalRole::CHIEF]);

// Récupérer les modèles rattachés de plusieurs types avec plusieurs rôles
$personnel = $this->rattachmentService->getRattachablesByTypesAndRoles($hospital, [Doctor::class, Nurse::class], [HospitalRole::DOCTOR, HospitalRole::STAFF]);

// Récupérer toutes les cibles d'un modèle
$hospitals = $this->rattachmentService->getTargets($doctor);

// Récupérer les cibles d'un type spécifique
$hospitals = $this->rattachmentService->getTargetsByType($doctor, Hospital::class);

// Récupérer les cibles par type et rôle
$hospitals = $this->rattachmentService->getTargetsByTypeAndRole(
    $doctor,
    Hospital::class,
    HospitalUserRole::DOCTOR
);

// Récupérer les cibles de plusieurs types avec plusieurs rôles
$targets = $this->rattachmentService->getTargetsByTypesAndRoles(
    $doctor,
    [Hospital::class, Pharmacy::class],
    [HospitalUserRole::DOCTOR, PharmacyUserRole::PHARMACIST]
);

// Paginer les cibles
$hospitals = $this->rattachmentService->getTargetsPaginated($doctor, 10, 2);

// Paginer les cibles par rôle
$hospitals = $this->rattachmentService->getTargetsByRolePaginated(
    $doctor,
    HospitalUserRole::DOCTOR,
    10,
    2
);

// Paginer les rattachables
$doctors = $this->rattachmentService->getRattachablesPaginated($hospital, 10, 2);

// Paginer les rattachables par rôle
$doctors = $this->rattachmentService->getRattachablesByRolePaginated(
    $hospital,
    HospitalUserRole::DOCTOR,
    10,
    2
);

// Paginer les rattachables par type
$doctors = $this->rattachmentService->getRattachablesByTypePaginated(
    $hospital,
    Doctor::class,
    10,
    2
);

// Compter les cibles
$total = $this->rattachmentService->countTargets($doctor);

// Compter les cibles par rôle
$total = $this->rattachmentService->countTargetsByRole($doctor, HospitalUserRole::DOCTOR);

// Compter les rattachables
$total = $this->rattachmentService->countRattachables($hospital);

// Compter les rattachables par rôle
$total = $this->rattachmentService->countRattachablesByRole($hospital, HospitalUserRole::DOCTOR);

// Mettre à jour le rôle
$this->rattachmentService->updateRole($doctor, $hospital, HospitalUserRole::ADMIN);

// Mettre à jour plusieurs rôles
$this->rattachmentService->updateRoleForMultiple(
    collect([$doctor1, $doctor2]),
    $hospital,
    HospitalUserRole::ADMIN
);

// Mettre à jour les métadonnées (remplacement)
$this->rattachmentService->updateMetadata($doctor, $hospital, [
    'consultation_hours' => '10:00-18:00',
]);

// Fusionner les métadonnées (conserve les existantes)
$this->rattachmentService->mergeMetadata($doctor, $hospital, [
    'availability' => 'Monday-Friday',
]);

// Synchronise tous les rattachements d'un modèle
$attachments = $this->rattachmentService->syncAttachments($doctor, [
    [
        'target' => $hospital1,
        'role' => HospitalUserRole::DOCTOR,
        'metadata' => ['primary' => true],
    ],
    [
        'target' => $hospital2,
        'role' => HospitalUserRole::DOCTOR,
    ],
    [
        'target' => $pharmacy,
        'role' => PharmacyUserRole::PHARMACIST,
        'metadata' => ['supplier' => true],
    ],
]);

// Les rattachements précédents qui ne sont pas dans la liste sont supprimés

// Rôles distincts pour ce modèle comme rattachable
$roles = $this->rattachmentService->getDistinctRolesForRattachable($doctor);

// Rôles distincts pour ce modèle comme cible
$roles = $this->rattachmentService->getDistinctRolesForTarget($hospital);

// Vérifier si des attachements existent entre deux types
$exists = $this->rattachmentService->hasAttachmentsBetweenTypes(
    Doctor::class,
    Hospital::class
);

// Récupérer tous les attachements entre deux types
$attachments = $this->rattachmentService->getAttachmentsBetweenTypes(
    Doctor::class,
    Hospital::class
);

// Supprimer tous les attachements entre deux types
$count = $this->rattachmentService->deleteAllAttachmentsBetweenTypes(
    Doctor::class,
    Hospital::class
);

class Doctor extends Model implements RattachmentInterface
{
    use HasRattachments;
}

$doctor = Doctor::find(1);
$hospital = Hospital::find(1);

// Rattacher
$doctor->attachTo($hospital, HospitalRole::DOCTOR);

// Vérifier
if ($doctor->isAttachedTo($hospital)) {
    // ...
}

// Récupérer les hôpitaux
$hospitals = $doctor->getTargetsByRole(HospitalRole::DOCTOR);

// Compter
$count = $doctor->countTargetsByRole(HospitalRole::DOCTOR);

// Détacher
$doctor->detachFrom($hospital);

// Synchroniser
$doctor->syncAttachments([
    ['target' => $hospital1, 'role' => HospitalRole::DOCTOR],
    ['target' => $hospital2, 'role' => HospitalRole::CHIEF],
]);

$user = User::find(1);

// Récupérer les cibles (ce que User a attaché)
$hospitals = $user->getTargetsByType(Hospital::class);

// Récupérer les rattachables (ce qui est attaché à User)
$specialties = $user->getRattachablesByType(Specialty::class);

// Vérifier les deux sens
if ($user->isAttachedTo($hospital)) {
    // User est attaché à Hospital
}

if ($hospital->isAttachedTo($user)) {
    // Hospital est attaché à User
}

// Compter les cibles
$totalHospitals = $user->countTargets();
$totalDoctors = $user->countTargetsByRole(HospitalRole::DOCTOR);

// Compter les rattachables
$totalUsers = $hospital->countRattachables();
$totalDoctors = $hospital->countRattachablesByRole(HospitalRole::DOCTOR);

class User extends Model implements RattachmentInterface
{
    use HasRattachments;

    // Amitié bidirectionnelle
    public function becomeFriendWith(User $friend): void
    {
        $this->attachTo($friend, FriendRole::FRIEND);
        $friend->attachTo($this, FriendRole::FRIEND);
    }

    public function unfriend(User $friend): void
    {
        $this->detachFrom($friend);
        $friend->detachFrom($this);
    }

    public function getFriends(): Collection
    {
        return $this->getTargetsByTypeAndRole(User::class, FriendRole::FRIEND);
    }

    public function getBestFriends(): Collection
    {
        return $this->getTargetsByTypeAndRole(User::class, FriendRole::BEST_FRIEND);
    }

    // Suivi unidirectionnel
    public function follow(User $user): void
    {
        $this->attachTo($user, FollowRole::FOLLOWER);
    }

    public function unfollow(User $user): void
    {
        $this->detachFrom($user);
    }

    public function isFollowing(User $user): bool
    {
        return $this->isAttachedTo($user);
    }

    public function getFollowers(): Collection
    {
        return $this->getRattachablesByRole(FollowRole::FOLLOWER);
    }

    public function getFollowing(): Collection
    {
        return $this->getTargetsByRole(FollowRole::FOLLOWER);
    }

    // Blocage
    public function block(User $user): void
    {
        $this->attachTo($user, BlockRole::BLOCKED);
    }

    public function blockMutually(User $user): void
    {
        $this->attachTo($user, BlockRole::BLOCKED);
        $user->attachTo($this, BlockRole::BLOCKED);
    }

    public function unblock(User $user): void
    {
        $this->detachFrom($user);
    }

    public function isBlocking(User $user): bool
    {
        return $this->isAttachedTo($user);
    }

    public function getBlockedUsers(): Collection
    {
        return $this->getTargetsByRole(BlockRole::BLOCKED);
    }
}

class User extends Model implements RattachmentInterface
{
    use HasRattachments;

    public function becomeFriendWith(User $friend): void
    {
        $this->attachTo($friend, FriendRole::FRIEND);
        $friend->attachTo($this, FriendRole::FRIEND);
    }

    public function unfriend(User $friend): void
    {
        $this->detachFrom($friend);
        $friend->detachFrom($this);
    }

    public function getFriends(): Collection
    {
        return $this->getTargetsByTypeAndRole(User::class, FriendRole::FRIEND);
    }

    public function getBestFriends(): Collection
    {
        return $this->getTargetsByTypeAndRole(User::class, FriendRole::BEST_FRIEND);
    }

    public function isFriendWith(User $user): bool
    {
        return $this->isAttachedTo($user);
    }
}

class User extends Model implements RattachmentInterface
{
    use HasRattachments;

    public function follow(User $user): void
    {
        $this->attachTo($user, FollowRole::FOLLOWER);
    }

    public function unfollow(User $user): void
    {
        $this->detachFrom($user);
    }

    public function isFollowing(User $user): bool
    {
        return $this->isAttachedTo($user);
    }

    public function getFollowers(): Collection
    {
        return $this->getRattachablesByRole(FollowRole::FOLLOWER);
    }

    public function getFollowing(): Collection
    {
        return $this->getTargetsByRole(FollowRole::FOLLOWER);
    }
}

class Post extends Model implements RattachmentInterface
{
    use HasRattachments;

    public function addTag(Tag $tag, TagRole $role): void
    {
        $this->attachTo($tag, $role, [
            'added_at' => now()->toDateTimeString(),
        ]);
    }

    public function removeTag(Tag $tag): void
    {
        $this->detachFrom($tag);
    }

    public function getTags(): Collection
    {
        return $this->getTargetsByType(Tag::class);
    }

    public function getPrimaryTags(): Collection
    {
        return $this->getTargetsByTypeAndRole(Tag::class, TagRole::PRIMARY);
    }

    public function syncTags(array $tags): Collection
    {
        return $this->syncAttachments($tags);
    }
}

class Doctor extends Model implements RattachmentInterface
{
    use HasRattachments;

    public function workAt(Hospital $hospital, HospitalRole $role): void
    {
        $this->attachTo($hospital, $role, [
            'start_date' => now()->toDateString(),
        ]);
    }

    public function leaveHospital(Hospital $hospital): void
    {
        $this->detachFrom($hospital);
    }

    public function promoteToChief(Hospital $hospital): void
    {
        $this->updateRoleFor($hospital, HospitalRole::CHIEF);
    }

    public function getHospitals(): Collection
    {
        return $this->getTargetsByType(Hospital::class);
    }

    public function beforeAttach(
        Model $other,
        EnumerableInterface $role,
        array $metadata,
        HookPosition $position
    ): void {
        Log::info('Doctor ' . $this->id . ' is being attached', [
            'hospital_id' => $other->getKey(),
            'role' => $role->getValue(),
        ]);
    }

    public function afterAttach(
        Model $other,
        EnumerableInterface $role,
        Model $attachment,
        HookPosition $position
    ): void {
        // Envoyer une notification à l'hôpital
        $other->notify(new DoctorJoinedNotification($this, $role));
    }
}

$user->attachTo($profile, ProfileRole::USER);
// User est le rattachable, Profile est le target
// "User a un Profile"

// ❌ Erreur
class User extends Model
{
    use HasRattachments;
}

// ✅ OK
class User extends Model implements RattachmentInterface
{
    use HasRattachments;
}

// ❌ Erreur
$user->attachTo($profile);

// ✅ OK
$user->attachTo($profile, ProfileRole::USER);

public function allowedTargets(): array
{
    return [
        Profile::class => [ProfileRole::USER, ProfileRole::ADMIN],
    ];
}

public function disallowedTargets(): array
{
    return [
        Profile::class => [ProfileRole::ADMIN],  // ✅ ADMIN est bloqué
    ];
}

// ✅ OK
$user->attachTo($profile, ProfileRole::USER);

// ❌ Erreur - ADMIN est bloqué
$user->attachTo($profile, ProfileRole::ADMIN);
// Role "admin" is disallowed

// ❌ Erreur - Circularité
class User implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],
        ];
    }
}

class Profile implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            User::class => [ProfileRole::USER],  // ❌ Circularité !
        ];
    }
}

// Exception: Circular relationship detected: User → Profile with role "user"
// and Profile → User with the same role.

// ❌ Erreur - Circularité sur uniqueTargets
class User implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],
        ];
    }

    public function uniqueTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],  // Un User ne peut avoir qu'un seul Profile
        ];
    }
}

class Profile implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            User::class => [ProfileRole::USER],
        ];
    }

    public function uniqueTargets(): array
    {
        return [
            User::class => [ProfileRole::USER],  // Un Profile ne peut appartenir qu'à un seul User
        ];
    }
}

// 💥 Exception: Circular unique constraint detected: User → Profile with role "user"
// and Profile → User with the same role.
// This creates a circular dependency. Define the unique constraint in only one direction.

// ❌ Erreur
$user->attachTo($user, FriendRole::FRIEND);
// Cannot attach a model to itself. App\Models\User 1 cannot be attached to itself.

// ✅ Recommandé
class User implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],
        ];
    }
}

class Profile implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [];  // ❌ Ne pas autoriser l'inverse
    }
}

class Profile implements RattachmentInterface
{
    public function disallowedTargets(): array
    {
        return [
            User::class => [ProfileRole::USER],  // ✅ Bloque l'inverse
        ];
    }
}

// ❌ À ÉVITER
class User implements RattachmentInterface
{
    public function allowedTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],  // ✅ Autorisé
        ];
    }

    public function disallowedTargets(): array
    {
        return [
            Profile::class => [ProfileRole::USER],  // ❌ Mais aussi bloqué !
        ];
    }
}

public function becomeFriendWith(User $friend): void
{
    $this->attachTo($friend, FriendRole::FRIEND);
    $friend->attachTo($this, FriendRole::FRIEND);
}
bash
php artisan vendor:publish --tag=rattachments-migrations
bash
php artisan migrate