Your first Entity
Creating entity requires a few things to start:
- defined mapping in your
dullahan_entityconfig - Ready to use class with defined fields (preferably corresponding to columns)
To fulfill the first requirement we will add new mapping to our Symfony config for Dullahan Entity Bundle:
dullahan_entity:
mappings:
main:
prefix: App\Entity
Thanks to this Dullahan Entity mappers know where to look when using API calls to manage our entities. It is not necessary if you aren't using the Entity API.
For the purpose of this explanation, I'm going to use entity generated by Maker Bundle representing table for saving information about a simple blog post.
This guide won't explain anything related Doctrine Bundle as this is just an example how we can combine table definitions with actual data manipulation and validation. If this example is hard to grasp have a read about Doctrine ORM.
The blog post will be made from identification number, author name, actual post content and secret content (private
information about the blog like next version). To create it we will use Symfony command make:entity:
php bin/console make:entity Post
This command will prompt us for required fields (author, content and secretContent), generate entity and repository file based on our answers. Here is our generated entity file which uses Doctrine mappings:
namespace App\Entity;
use App\Repository\PostRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $author = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $secretContent = null;
// For now we are skipping getters and setters but they are required!
Now let's add our attributes to make this a Dullahan managed entity:
use App\Constraint\PostConstraint; // New!
use Dullahan\Entity\Domain\Attribute as Dullahan; // New!
use Dullahan\Entity\Port\Domain\OwnerlessManageableInterface; // New!
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[Dullahan\Entity(PostConstraint::class)] // New!
class Post implements OwnerlessManageableInterface // New!
{
#[Dullahan\Field] // New!
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[Dullahan\Field] // New!
#[ORM\Column(length: 255)]
private ?string $author = null;
#[Dullahan\Field] // New!
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $secretContent = null;
Each field is required to have specific getter and setter for system to work properly. We are just skipping them to make this example a little more compact and easier to wrap your head around but don't forget to add them!
At the example above we where able to define table structure using Doctrine library, mark specific fields as public/manageable, specify class responsible for validation and ownership strategy of the entity.
The field $secretContent was specifically not marked with Dullahan\Field attribute, as we don't want it to be manageable by the user.
Now we just need validation class App\Constraint\PostConstraint, for example you can see this.
The entity is finished and ready to be managed via API calls. If you want to have a deeper understanding keep reading!
Validation
When creating your own Manageable Entity you have to define validation constraints for the creation and update. This information is required when marking the class as Dullahan Entity as you can see in the example above.
The validation class must implement EntityValidateConstraintInterface and follow its specifications. Here is an example for the Post entity:
namespace App\Constraint;
use Dullahan\Entity\Port\Domain\ConstraintInheritanceAwareInterface;
use Dullahan\Entity\Port\Domain\EntityValidateConstraintInterface;
use Dullahan\Main\Service\Util\ConstraintUtilService;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraint;
class PostConstraint implements EntityValidateConstraintInterface
{
public static function create(): Assert\Collection
{
return new Assert\Collection(self::getConstraint());
}
public static function update(): Assert\Collection
{
return new Assert\Collection(ConstraintUtilService::constraintToOptional(self::getConstraint()));
}
/**
* @return array<string, array<Constraint>>
*/
protected static function getConstraint(): array
{
return [
'author' => [
new Assert\NotBlank(['message' => 'Missing author\'s name']),
new Assert\Type([
'type' => 'string',
'message' => 'Author must be a string',
]),
],
'content' => [
new Assert\NotBlank(['message' => 'Missing content']),
new Assert\Type([
'type' => 'string',
'message' => 'Content must be a string',
]),
],
];
}
}
As you can see there are two types on validations: create and update. For creating a Post we are requiring all possible fields but for update all of them are optional - allowing for partial updates. It is for you to decide what must be provided and when.
ConstraintUtilService::constraintToOptionalA helper method for converting a constraint Collection to only optional fields. Thanks to it, you don't have to define the same field set twice.
Ownership
There are currently three ownership strategies:
- ownerless - any logged user can manage this entity - useful for centralized data services where everyone works on the same data
- owned - only assigned user can manage this entity
- transferable - entity verifies itself via defined method if passed user can manage it
To mark entity to be handled by specific strategy we use interfaces:
Ownerless
As shown on the example above we are using OwnerlessManageableInterface to mark this entity as free to manage
by everyone:
use Dullahan\Entity\Domain\Attribute as Dullahan;
use Dullahan\Entity\Port\Domain\OwnerlessManageableInterface;
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[Dullahan\Entity(PostConstraint::class)]
class Post implements OwnerlessManageableInterface
{
#[Dullahan\Field, ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
// Must implement `getId` method!
public function getId(): ?int
{
return $this->id;
}
}
Owned
For entity to have an owner it must implement ManageableInterface and User Bundle must be loaded:
use Dullahan\Entity\Domain\Attribute as Dullahan;
use Dullahan\Entity\Port\Domain\ManageableInterface;
use Dullahan\User\Domain\Entity\UserData;
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[Dullahan\Entity(PostConstraint::class)]
class Post implements ManageableInterface
{
#[Dullahan\Field, ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[Dullahan\Field, ORM\ManyToOne, ORM\JoinColumn(nullable: false)]
private ?UserData $userData = null;
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->userData?->getUser();
}
public function setUser(?User $user): self
{
$this->userData = $user?->getData();
return $this;
}
public function isOwner(User $user): bool
{
if (!$this->getUser()?->getId() || !$user->getId()) {
return false;
}
return $this->getUser()->getId() === $user->getId();
}
public function setOwner(User $user): self
{
$this->setUser($user);
return $this;
}
public function getUserData(): ?UserData
{
return $this->userData;
}
public function setUserData(?UserData $userData): static
{
$this->userData = $userData;
return $this;
}
}
As QOL for this bundle, it comes with a helper trait UserDataRelationTrait to make defining manageable entities
easier.
use Dullahan\Entity\Domain\Attribute as Dullahan;
use Dullahan\Entity\Port\Domain\ManageableInterface;
use Dullahan\User\Domain\Entity\UserData;
use Dullahan\User\Domain\Trait\UserDataRelationTrait;
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[Dullahan\Entity(PostConstraint::class)]
class Post implements ManageableInterface
{
use UserDataRelationTrait;
#[Dullahan\Field, ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[Dullahan\Field, ORM\ManyToOne, ORM\JoinColumn(nullable: false)]
private ?UserData $userData = null;
public function getId(): ?int
{
return $this->id;
}
public function getUserData(): ?UserData
{
return $this->userData;
}
public function setUserData(?UserData $userData): static
{
$this->userData = $userData;
return $this;
}
}
As defined in the User Bundle guide when creating any database schemas, for new fields or relations it is recommended to user user_data table instead of internal user table.
Transferable
A transferable entity doesn't hold information about the owner but defines its own way of checking if current user can manage it. For example it can validate the owner using its relations:
use Dullahan\Entity\Domain\Attribute as Dullahan;
use Dullahan\Entity\Port\Domain\TransferableOwnerManageableInterface;
use Dullahan\User\Domain\Entity\UserData;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
#[Dullahan\Entity(CommentConstraint::class)]
class Comment implements TransferableOwnerManageableInterface
{
#[Dullahan\Field, ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[Dullahan\Field, ORM\ManyToOne, ORM\JoinColumn(nullable: false)]
private ?UserData $userData = null;
#[Dullahan\Field, ORM\ManyToOne(inversedBy: 'comments'), ORM\JoinColumn(nullable: false)]
private ?Post $post = null;
public function getId(): ?int
{
return $this->id;
}
public function isOwner(User $user): bool
{
return $this->post?->getUser()?->getId() === $user->getId();
}
}
Custom
If you want to implement your own ownership verification method you will need to listen for VerifyEntityOwnership event and determinate if provided entity should be handled by your custom implementation.
In this example I've created new interface RandomManageableInterface to mark Entities which should be handled by randomized algorithm:
use Dullahan\Entity\Port\Domain\IdentityAwareInterface;
interface RandomManageableInterface extends IdentityAwareInterface {
public function isOwner(): bool;
}
Then I listen for VerifyEntityOwnership event and if entity implements this interface I run my verification algorithm:
class RandomOwnershipVerificationListener {
public function onVerifyEntityOwnership(VerifyEntityOwnership $event): void
{
if (!$event->entity instanceof RandomManageableInterface) {
return;
}
$event->isValid = random_int(1, 10) > 5;
}
}