Objectiphy Mapping

Mapping definition classes which can be loaded from either attributes or annotations.

To use Objectiphy mapping definitions, include a use statement at the top of your entity class. There are three attributes or annotations available: @Table, @Column, and @Relationship:

use Objectiphy\Objectiphy\Mapping\Table;
use Objectiphy\Objectiphy\Mapping\Column;
use Objectiphy\Objectiphy\Mapping\Relationship;

The Objectiphy attributes and annotations support the following attributes:

Table

Use this on the class itself (not the properties), to identify the table name, and perhaps a custom repository and whether to insist that the custom repository is always used.

  • name: Name of the main table that stores information for this entity.

  • repositoryClassName: Name of custom repository class to use when loading this entity.

  • alwaysLateBind: Whether or not to force even eager loaded one-to-one and many-to-one relationships to late bind so that they can use the custom repository. Defaults to false. Only specify true here if absolutely necessary, as it will require a separate database call to populate this entity wherever it appears (it cannot be eager loaded with joins).

Column

Use a column annotation for properties that represent scalar values (are not arrays or objects other than DateTimeInterface types) - otherwise, use a relationship annotation (see below). You don't necessarily have to specify any of these attributes - name can be guessed, type will default to string, and the rest would only apply to certain properties (eg. the primary key). So you could just use @Column on its own for most scalar properties.

  • name: Name of the column that stores data for this property. You can use the special value 'IGNORE' to skip over this property completely. If you don't specify a name, Objectiphy can guess the name of the column based on a naming strategy (by default, pascal or camel to snake case).

  • type: Data type name. If the property has a data type, that is not a string, you should also populate this attribute. Built-in data types that are supported are as follows (note, you can also implement your own data types and conversions by injecting a custom class implementing DataTypeHandlerInterface to the repository factory):

  • format: A format string to use with DateTimes or sprintf. If the value is a DateTime or DateTimeImmutable, it can be converted to a formatted string by specifying the format here. If the value is a string, it can be processed with sprintf by specifying a pattern here (and the value will be the argument). NOTE: If you want to round a number that is stored in the database as a decimal to 2 decimal places, mark the property as a string, and use %0.2f as the format. This will prevent unexpected issues with floating point precision conversions.

  • isPrimaryKey: Boolean flag indicating primary key.

  • autoIncrement: Whether or not the primary key value auto increments. Only applies if isPrimaryKey is set to true, and defaults to true. If set to false, you must supply the primary key value yourself when inserting, and Objectiphy will insert if possible, or update if the key already exists.

  • isReadOnly: Whether or not to prevent this field being persisted. If null (which is the default), this will evaluate to false for everything EXCEPT scalar joins (it is safer to assume scalar joins are read-only as they are usually used to look up a value in a cross-reference table), or properties that use aggregate functions (as these are calculated based on other data, they cannot be set arbitrarily - more on these below). You can override the default behaviour by specifying a value of either true or false on the mapping definition.

  • aggregateFunctionName: Name of aggregate function to use to calculate the value of this property. Eg. "AVG", "SUM", "MAX". If an aggregate function is specified, the value of the property is always read-only.

  • aggregateCollectionPropertyName: Name of the to-many property that holds the collection of records on which to perform the aggregate function. This should be the name of a property on the same object, and should hold a collection of child entities (ie. have a one-to-many or many-to-many relationship with the parent class).

  • aggregatePropertyName: Name of the property on the child collection to pass as a parameter to the aggregate function, if applicable. This should be the name of a property on the child class that is the subject of the aggregate function. If not specified, the primary key of the child class will be used.

  • aggregateGroupBy: Name of the property to group by, if not the primary key of the class this annotation appears on.

    Usually, aggregate functions are grouped by the primary key of the parent, but if you want to use different grouping, you can specify it here.

  • dataMap: A list of key/value pairs to translate values from the database to different values in your entity or result set. Typically used to convert a code stored in the database to a human readable description for use in your entity. When loading an entity, if a defined key is encountered in this column in the database, it will be translated to the corresponding value when the result is returned. Likewise, when saving an entity, the value will be translated back into the corresponding key before it is persisted. If the same description is used on more than one key, the first one encountered will be used for persistence. As well as straightforward key/value pairs, you can also do value comparisons and have a catch-all ELSE clause to return a specific value for a range of possible values in the database. This is only possible when reading data though - if you try to save an entity, and the value it holds corresponds to an ELSE clause or matches a range of values in the data map, it will not be possible to determine which value to use, so the value will not be updated at all. To specify a range of values, split the value part into a child array containing elements for operator and value, for example: [ '99' => ['operator' => '<=', 'value' => 'Less than 100'], '100' => ['One hundred!'], '101' => ['operator' => '>=', 'value' => 'More than 100'], ] To use an ELSE clause, just specify 'ELSE' as the key, like this: [ '1' => 'One', '2' => 'Two', 'ELSE' => 'Something else', ]

  • function: An SQL function to use instead of reading data directly from a column. Typically, a property value will map directly to the value from a database column, however, it is possible to specify a function to use to populate the value instead. If you do this, the value becomes read-only, since it is not possible to automatically reverse a function. The function must be supported by the database engine (typically MySQL), but may include property paths, literal values, and references to database tables and columns. For example, a DateTime property could be populated by concatenating a date column and a time column: CONCAT(`date_column`, ' ', `time_column`). Please bear in mind that if this is used on a child object, any paths used must be relative to the parent (or use unambiguous column names), which makes this of limited use. It might get you out of a scrape though!

Relationship

Use a relationship annotation where the property relates to a child object or an array or collection of child objects.

  • relationshipType: One of: one_to_one, one_to_many, many_to_one, many_to_many, or scalar. As far as basic reading and writing is concerned, one-to-one and many-to-one are treated in exactly the same way - one-to-one is not enforced (it will allow many-to-one relationships to exist on a one-to-one mapping). This is because it would take significant overhead to check whether there are other records in the database that violate the join type, and it usually doesn't really matter (if it does matter, you can enforce it programatically). The only exception there is that if orphan removal is used, many-to-one relationships will check whether any other parent owns a child before it is treated as an orphan. Scalar means the relationship exists to populate a single scalar value from a related table, not an entire child object (eg. to get a string description for a particular code) - see Scalar Joins.

  • type: Data type name - for scalar join relationship types only. Supports data types in the same way as the @Column annotation, as described above.

  • childClassName: Class of the child object. Does not have to be fully qualified in the annotation, as long as there is a use statement for it on the entity.

  • mappedBy: Property of child class which owns the relationship, if applicable. If the relationship is owned by the child class, this specifies the property on the child class that holds the mapping. Required for one-to-many relationships, but only necessary on one-to-one if the child owns the relationship (ie. the value that joins the two entities is stored in a column of the table for the child class).

  • lazyLoad: Whether or not to defer loading the data until it is accessed. If omitted, one-to-one and many-to-one are eager loaded (with joins), and one-to-many and many-to-many are lazy loaded (as they require a separate database call anyway).

  • joinTable: Name of target table to join to. For a normal parent/child relationship, you do not need to specify this, as it will be picked up from the class mapping for the child entity (but you can override that here if required). This attribute is normally used to create a scalar join (to grab a single scalar value from a joined table without needing an entire child entity). For scalar joins, as there is no child entity, all of the join mapping information must be specified on the source entity.

  • sourceJoinColumn: Name of column on source table (parent) to use in the join. This is the same as the name attribute on Doctrine's JoinColumn, and is useful when creating scalar joins where the column name holding the value is not the column name used in the join (see example, below).

  • targetJoinColumn: Name of column on target table to use in the join. Defaults to id. NOTE: This is the same as the referencedColumnName attribute on Doctrine's JoinColumn annotation (NOT the name attribute on Doctrine's JoinColumn, which refers to the column on the local table rather than the target table).

  • targetScalarValueColumn: Name of the column that holds the value for a scalar join property. The columns involved in a join to grab a scalar value do not usually hold the actual value you want to read. Specify the column that holds the value here.

  • bridgeJoinTable: Name of the bridging table that links two types of record in a many-to-many relationship. A many-to-many relationship requires a table that records the primary keys of the records on each side of the relationship, and the table name must be defined - it is usually best to follow the naming convention: source property + underscore + target property (eg. student_course), to give Objectiphy the best chance of being able to guess the columns to map to without you having to define them all in full (see bridgeSourceJoinColumn and bridgeTargetJoinColumn, below).

  • bridgeSourceJoinColumn: Name of the foreign key column on the bridging table that stores the value of the primary key of the source (parent) record. ie. the one that holds the mapping information for the relationship. If omitted, Objectiphy will try to guess it based on the bridgeJoinTable name and the de-pluralised names of the properties involved in the join.

  • bridgeTargetJoinColumn: Name of the foreign key column on the bridging table that stores the value of the primary key of the target (child) record. ie. the one that specifies a mappedBy attribute, pointing to the property on the other class that holds the mapping definition for the relationship. If omitted, Objectiphy will try to guess it based on the bridgeJoinTable name and the de-pluralised names of the properties involved in the join.

  • joinType: One of: INNER, LEFT, RIGHT. Defaults to LEFT if none specified.

  • isEmbedded: Whether the property is a value object whose columns are on this (parent) object's table. Defaults to false.

  • embeddedColumnPrefix: Prefix to apply to the column names for embedded objects. Typically used where more than one embedded object of the same type exists on a class.

  • orderBy: Properties to use for ordering the child objects in a to-many relationship - eg. ['name' => 'ASC', 'type' => 'ASC'] (attributes) or {"name"="ASC","type"="DESC"}(annotations). Ignored if property does not relate to a to-many relationship.

  • collectionClass: Either 'array' or the name of a collection class to use for storing child objects in a to-many-relationship. If a custom class is specified, it must implement \Traversable and it should either be possible to instantiate by passing a PHP array to the constructor as the one and only required argument, or you must provide a factory to create the collection class. If a factory is needed to create your collection instances, it must implement CollectionFactoryInterface and you must inject your factory into the repository factory (using the setCollectionFactory method) before creating repositories (if using a factory, the collection does not have to take an array in the constructor - it is up to you how to populate it).

  • cascadeDeletes: Whether to delete child objects if the parent is deleted. Defaults to false. Will only take effect if entity deletes are not disabled by the configuration options.

  • orphanRemoval: Whether to delete child objects if they are removed from their parent. Defaults to false. Will only take effect if relationship deletes are not disabled by the configuration options.

Examples:

Table

You can optionally specify the database name

#[Table(name: 'insurance.policy')]
class Policy
{
   //...
}

Column annotation

#[Column(name: 'dob', type: 'date')]
protected \DateTime $dateOfBirth;

Aggregate function column annotations

#[Relationship(
    collectionClass: TestCollection::class,
    childClassName: TestPet::class,
    mappedBy: 'child',
    relationshipType: 'one_to_many'
)]
public array $pets;

#[Column(
    aggregateFunctionName: 'COUNT',
    aggregateCollectionPropertyName: 'pets'
)]
public $numberOfPets;

#[Column(
    aggregateFunctionName: 'SUM',
    aggregateCollectionPropertyName: 'pets',
    aggregatePropertyName: 'weightInGrams'
)]
public $totalWeightOfPets;

In the above example, the properties will be populated according to the results of the aggregate functions, and you can filter by their values using criteria on the findBy methods, or in the where clause of a query built with the query builder. Objectiphy will automatically move them to the HAVING clause rather than the WHERE clause when these properties appear in criteria. Note however, that if you are writing a custom query using aggregate functions and the query builder (not relying on these properties with their annotations), you must use the having method of the query builder, not the where method - Objectiphy does not interfere with your custom queries.

As you might expect, properties that use aggregate functions are always read-only.

Relationship annotations

See the defining relationships section for more information and examples.

/**
 * Example of a one-to-one relationship:
 */
#[Relationship(
    sourceJoinColumn: 'policyholder_id',
    childClassName: Customer::class,
    relationshipType: 'one_to_one',
    joinType: 'INNER'
)]
protected Customer $customer;

/**
 * Example of a many-to-many relationship:
 */
 #[Relationship(
     relationshipType: 'many_to_many',
     childClassName: File::class,
     bridgeJoinTable: 'policy_file'
 )]
 protected $files;
 
 /**
 * Example of a more verbose many-to-many relationship
 * (the column definitions are optional if they follow
 * a sensible naming convention based on the class names
 * and the classes have primary key properties defined):
 */
 #[Relationship(
     relationshipType: 'many_to_many',
     childClassName: Document::class,
     bridgeJoinTable: 'policy_document',
     sourceJoinColumn: 'id',
     bridgeJoinSourceColumn: 'policy_id',
     targetJoinColumn: 'id',
     bridgeTargetJoinColumn: 'document_id'
 )]
 protected $document;

Scalar join relationship annotation

See the scalar joins page for more information.

#[Relationship(
    relationshipType: 'scalar',
    scalarTargetValueColumn: 'occupation.description',
    joinTable: 'occupation',
    sourceJoinColumn: 'occupation_code',
    targetJoinColumn: 'code'
)]
protected string $occupation;

Last updated