- Issue created by @godotislate
- Merge request !12161Issue #3524759: Introduce exception type that allows for enum codes. → (Open) created by godotislate
While working on
📌
Remove exception when accessing a non-existing field with ContentEntityInterface::get()
Active
, I ran into a small complication on handling thrown \InvalidArgumentExceptions thrown by Drupal\Core\Entity\ContentEntityBase::getTranslatedField()
:
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
}
// Populate $this->fields to speed-up further look-ups and to keep track of
// fields objects, possibly holding changes to field values.
if (!isset($this->fields[$name][$langcode])) {
$definition = $this->getFieldDefinition($name);
if (!$definition) {
throw new \InvalidArgumentException("Field $name is unknown.");
}
To handle the unknown field exception separately from the removed translation exception, the logic in the catch
block would need to be based a string match on the thrown exception message, which can be brittle to changes to those messages. An alternative would be to instantiate the exception objects with different codes, so that the logic can use those, but exception codes are integers. Assigning what integers should be associated with specific errors can be arbitrary and nonobvious. Granted, the integer codes could be assigned to constants that are more self-documenting, but even then it's hard to guarantee to that integer codes would be unique.
This can be resolved by creating and using Exception classes that leverage PHP enum cases as exception "codes".
Introduce an interface for enums:
interface ExceptionCodeInterface {
/**
* Returns exception code as an integer.
*/
public function getCode(): int;
}
Example enum:
enum InvalidFieldCode implements ExceptionCodeInterface {
case RemovedTranslation;
case UnknownField;
/**
* {@inheritdoc}
*/
public function getCode(): int {
return match ($this) {
// These codes aren't important in this example.
InvalidFieldCode::RemovedTranslation => 1,
InvalidFieldCode::UnknownField => 2,
};
}
}
Interface for new exception classes:
interface ExceptionWithEnumCodeInterface extends \Throwable {
/**
* Gets the enum exception code.
*/
public function getEnumExceptionCode(): ?ExceptionCodeInterface;
}
Base class for exception:
class ExceptionWithEnumCodeBase extends \Exception implements ExceptionWithEnumCodeInterface {
public function __construct(string $message = '', protected readonly ?ExceptionCodeInterface $enumCode = NULL, ?\Throwable $previous = NULL) {
$code = $enumCode?->getCode() ?? 0;
parent::__construct($message, $code, $previous);
}
/**
* {@inheritdoc}
*/
public function getEnumExceptionCode(): ?ExceptionCodeInterface {
return $this->enumCode;
}
}
Example exception class
class InvalidFieldArgumentException extends ExceptionWithEnumCodeBase {}
Then, rewriting the ContentEntityBase code above, it could be something like:
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
throw new InvalidFieldArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.", InvalidFieldCode::RemovedTranslation);
}
// Populate $this->fields to speed-up further look-ups and to keep track of
// fields objects, possibly holding changes to field values.
if (!isset($this->fields[$name][$langcode])) {
$definition = $this->getFieldDefinition($name);
if (!$definition) {
throw new InvalidFieldArgumentException("Field $name is unknown.", InvalidFieldCode::InvalidFieldArgumentException);
}
with the catch block:
catch (InvalidFieldArgumentException $e) {
if ($e->getEnumExceptionCode() === InvalidFieldCode::UnknownField) {
// Do specific handling here.
}
}
Interface/class/method names could use some work, but that's the idea.
Active
11.0 🔥
batch system