Using PHP enums as method calls
Dynamically mapping method calls to existing typed values.
Motivation
Large parts of the conjoon backend are currently being reworked to meet compliance with the **JSON:API**. This is so the webmail’s RESTful API provides consistency between requests and responses across various endpoints and provides a familiar environment for developers already using the JSON:API (or similar HATEOAS driven APIs) with other projects.
One challenge in adopting parts of the specification was not only the exclusion of fields (which ultimately culminated in the RFC for the relfield-extension ), but also in finding a way to pass meaningful filter-configurations from the client to the server.
Right now, the client can pass filter options in query parameters. Those query parameters need to be validated and get translated into their OOP representation, so that string-representations of the Filter-objects can be passed to connected IMAP servers in a format the protocol understands.
What are filters, anyway? (Spoiler: Expressions, of course)
A filter (or search query) consists of logical, **functional **and relational expressions, and an expression in turn has operands and **operators (**hint: ****an operator can also be an expressions).
The following represents a request for all emails that have a date set to the 9th of October 2022 or before (missing percent-encoded characters in favor of readability):
GET /MessageItems?filter={"<":{"date":1665375431}} HTTP/1.1
The above example has a *relational *operator (<
, less than), and has two operands: A name representing the name of the field the filter applies to (date), and an integer value representing the timestamp for comparison (1665375431).
The OOP modelling approach
Mathematical expressions are fairly easy to model. Their symbols can be abstracted into the following **UML **diagram:
An expression can be a
-
a Relational Expression providing relational operators:
<, >, <=, >=, ==, !=
-
a Logical Expression providing logical operators: &&, ||, !
-
a Functional Expression providing arbitrary function names, such as IN
Furthermore, each expression is associated with one or more Operands: An expression using a logical operator representing a negation (!) has only one operand, while a logical *disjunction *or *conjunction *must have two or more operands:
! 5 // logical negation operator has one operand
// a logical conjunction can have an arbitrary
// number of operands:
true && false && true
// logical disjunction with two operands
true || false
Further implementation details of how php-lib-conjoon models Expressions would go beyond the scope of this article. We’ll focus on how to use them in the code. Here’s an example for an expression that represents date < 1665375431. Note how the order of parameters for the constructor is in accordance with the **Polish Notation**, starting with the operator, then followed by the operands (< date 1665375431):
$expression = new RelationalExpression(
RelationalExpression::LESS_THAN,
new VariableName("date"),
new Value(1665375431)
);
$expression->toString() // produces "date < 1665375431"
Silence is golden: Refactoring into a factory
The above example is — while still readable and maintainable — too verbose. Code readability is king (see also my article about fluent interfaces) so let’s pour some syntactical sugar into the code:
$expression = RelationalExpression::lessThan(
VariableName::make("date"),
Value::make(1665375431)
);
This would allow us to omit calls to the constructor of the RelationalExpression, while still getting an expression as a return value from the factory method .
However, this would make it also necessary to implement static methods matching available operators used by the Expression-specific we’re currently using. Given six relational operators >, <, >=, <=,=, !=
, we’d have to add six factory methods to our class (while prototyping, without refactoring and code optimizations):
class RelationalExpression extends Expression {
/**
* Constructor.
*/
public function __construct(
RelationalOperator $operator,
OperandList $operands
) {
// ...
}
/**
* Builds a RelationalExpression for the
* RelationalOperator::LESS_THAN
*
* @param Operand $lft
* @param Operand $rt
*
* @return RelationalExpression
*/
public static function lessThan(
Operand $lft,
Operand $rt
): RelationalExpression
{
$operands = OperandList::make($lft, $rt);
return new self(RelationalOperator::LESS_THAN, $operands);
}
// ... additional implementations for the remaining operators
}
However, adding a new operator would require us to also add another function to the RelationalExpression —dependencies pop up where they shouldn’t and the code base grows unnecessarily, containing redundant code.