Integration with app resources

The bundle ends where business logic begins. Entities like Contribuyente, Repo, or Project belong to the app — but they often need to participate in the identity model (belong to an org, have collaborators with roles). The bundle provides interfaces and traits for this.

OrganizationOwnedInterface + Trait

“This entity belongs to an organization.”

use Derafu\IdentityBundle\Contract\Integration\OrganizationOwnedInterface;
use Derafu\IdentityBundle\Entity\Trait\OrganizationOwnedTrait;

#[ORM\Entity]
#[ORM\Table(name: 'contribuyentes')]
class Contribuyente implements OrganizationOwnedInterface
{
    use OrganizationOwnedTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $rut;

    // ... app-specific fields
}

The trait adds a ManyToOne to OrganizationInterface (resolved to your concrete Organization at runtime). The field is nullable by default — a Contribuyente can exist without an organization (personal account pattern). Add nullable: false on your entity if org ownership is mandatory.

The ResourcePermissionVoter uses this interface for cascading: if a user doesn’t have direct resource-level access, the voter automatically checks their org-level permission on the resource’s owning organization.

UserOwnedInterface + Trait

“This entity belongs directly to a user.”

use Derafu\IdentityBundle\Contract\Integration\UserOwnedInterface;
use Derafu\IdentityBundle\Entity\Trait\UserOwnedTrait;

class Contribuyente implements OrganizationOwnedInterface, UserOwnedInterface
{
    use OrganizationOwnedTrait;
    use UserOwnedTrait;
    // ...
}

Both traits are composable. A single entity can be owned by a user OR by an organization (or both, with app logic deciding which applies).

ResourceAccessibleInterface + BaseResourceAccess

“This entity supports per-resource collaborators with roles.”

Step 1: Create the access pivot entity

use Derafu\IdentityBundle\Entity\BaseResourceAccess;

#[ORM\Entity]
#[ORM\Table(name: 'contribuyente_access')]
#[ORM\UniqueConstraint(columns: ['user_id', 'contribuyente_id'])]
class ContribuyenteAccess extends BaseResourceAccess
{
    #[ORM\ManyToOne(targetEntity: Contribuyente::class, inversedBy: 'accesses')]
    #[ORM\JoinColumn(name: 'contribuyente_id', nullable: false, onDelete: 'CASCADE')]
    private Contribuyente $contribuyente;

    public function __construct(UserInterface $user, RoleInterface $role, Contribuyente $contribuyente)
    {
        parent::__construct($user, $role);
        $this->contribuyente = $contribuyente;
    }

    public function getContribuyente(): Contribuyente { return $this->contribuyente; }
}

Each resource type gets its own table — real FK integrity, no polymorphic hacks.

Step 2: Implement the interface on the resource

use Derafu\IdentityBundle\Contract\Integration\ResourceAccessibleInterface;

class Contribuyente implements OrganizationOwnedInterface, ResourceAccessibleInterface
{
    use OrganizationOwnedTrait;

    #[ORM\OneToMany(mappedBy: 'contribuyente', targetEntity: ContribuyenteAccess::class,
                    cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $accesses;

    public function __construct()
    {
        $this->accesses = new ArrayCollection();
    }

    public function getResourceAccesses(): Collection
    {
        return $this->accesses;
    }
}

Step 3: Use it

// In a controller:
#[IsGranted('write', subject: 'contribuyente')]
public function edit(Contribuyente $contribuyente): Response { ... }

The ResourcePermissionVoter kicks in, iterates the Contribuyente’s access entries for the current user, and checks permissions. If no direct access is found, it cascades to org-level and then global.

Summary

Interface Trait What it adds
OrganizationOwnedInterface OrganizationOwnedTrait $organization ManyToOne
UserOwnedInterface UserOwnedTrait $owner ManyToOne
ResourceAccessibleInterface (manual collection) Per-resource RBAC
ResourceAccessInterface BaseResourceAccess The access pivot entity
On this page

Last updated on 16/04/2026 by Anonymous