I am looking for advice on what the best practices for scenario below might be.
Scenario
I have some entities that use what I call "data as part of the schema": their data is part of the software and are not to be created/edited/deleted by the software during normal operation.
I'm using these to provide fixed lists of choices for other entities without using enum fields - because those are really part of the column definition and if I need to alter the column in any other way than to append a new value it results in a full table copy.
Such an entity is defined like this - readOnly=true parameter, manually assigned ID, unique slug, unique user-visible name, and private constructor. This isn't a good candidate for fixtures because it's not test data (talking about how Doctrine Fixtures wants to delete my "data as schema" is for another SO question)
/**
* @ORM\Entity(readOnly=true)
*/
class SubmissionStatus
{
/**
* @var int
* @ORM\Column(type="integer")
* @ORM\Id()
*/
protected $id;
/**
* @var string
* @ORM\Column(type="string", length=191, unique=true)
*/
protected $slug;
/**
* @var string
* @ORM\Column(type="string", length=191, unique=true)
*/
protected $name;
private __construct() {}
// ... other fields
}
I manage the values using Doctrine Migrations:
final class Version20181023132831 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$statuses = [
["id" => 1, "name" => "Pending", "slug" => "pending"],
["id" => 2, "name" => "Reported", "slug" => "reported"],
["id" => 3, "name" => "Rejected", "slug" => "rejected"],
["id" => 4, "name" => "Accepted", "slug" => "accepted"],
];
foreach ($statuses as $status) {
$this->addSql("INSERT INTO submission_status (id, name, slug) VALUES (:id, :name, :slug WHERE id = :id", [
"id" => $status['id'],
"name" => $status['name'],
"slug" => $status['slug'],
]);
}
}
}
I do UPDATE
and DELETE
in future migrations if I need to update these values - also massaging existing Foreign Keys referring to these rows if necessary.
Other entities have @ORM\ManyToOne
relationships to this entity.
The problem
What is the best way to refer to these in code?
Not all of these entities will be selected by users in ChoiceType
fields in forms or come in through API calls. Some of them will be used just in code like $otherEntity->setStatus(...)
Current solution
Currently we've settled on doing this:
We're storing constants in the entity class for the IDs and slugs.
class SubmissionStatus
{
const PENDING_ID = 1;
const PENDING_SLUG = 'pending';
const REPORTED_ID = 2;
const REPORTED_SLUG = 'reported';
const REJECTED_ID = 3;
const REJECTED_SLUG = 'rejected';
const ACCEPTED_ID = 4;
const ACCEPTED_SLUG = 'accepted';
}
When we need to setStatus
, we make database lookups:
$status = $em->getRepository(SubmissionStatus::class)->findOneBy(['slug' => SubmissionStatus::ACCEPTED_SLUG])
$otherEntity->setStatus($status);
Question
Is there are better way to do this?
I'm not a big fan of writing all that getRepository->findOneBy
boilerplate all the time, especially if I need to reference several of this kind of entities. I would like something short, easy and quick to write when doing it repeatedly.
I would also prefer not to have to change too many places when I make changes to the values. One in the migration and one in the class constants is quite enough.
What I've thought of so far
I can't add a helper function to the entity itself (e.g. setStatusAccepted()
or setStatus(SubmissionStatus::ACCEPTED_SLUG)
) because I can't look up the referred entity from there.
An injected service which does a lookup on creation and stores references. Its usage would be e.g. $entity->setStatus($statusRef->accepted)
, $entity->setStatus($statusRef->get(SubmissionStatus::ACCEPTED_SLUG))
. The first option breaks IDE code completion. The second one is a bit too long, even though shorter than $em->getRepository->find