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 |