Simple Relation

An important part of any ORM engine is the ability to handle relations between objects. In order to do so, we will use the cycle/annotated package to describe the relation.

Deeper review of different relations, their options, and select methods will be given in further sections.

Describe Entity

First we have to create two entities we want to relate:

/**
 * @Entity
 */
class User
{
    /**
     * @Column(type="primary")
     * @var int
     */
    protected $id;

    /**
     * @Column(type="string")
     * @var string
     */
    protected $name;

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }
}

And the entity we want to relate to:

/**
 * @Entity
 */
class Address
{
    /**
     * @Column(type="primary")
     * @var int
     */
    protected $id;

    /**
     * @Column(type="string")
     * @var string
     */
    protected $city;

    public function getId(): int
    {
        return $this->id;
    }

    public function getCity(): string
    {
        return $this->city;
    }

    public function setCity(string $city): void
    {
        $this->city = $city;
    }
}

To relate our entities we have to add a new property to one of them and annotate it properly. We should also add getters and setters.

/**
 * @Entity
 */
class User
{
    // ...

    /**
     * @HasOne(target="Address")
     * @var Address|null
     */
    protected $address;

    // ...

    public function getAddress(): ?Address
    {
        return $this->address;
    }

    public function setAddress(Address $address)
    {
        $this->address = $address;
    }
}

Once you update the schema and sync your database schema (or run migrations), you are ready to use this relation.

ORM will automatically create a FK on address.user_id. We will describe how to alter this value later.

Store with related entity

To store the related entity with its parent simply persist the object which defines the relation:

$user = new User();
$user->setName("Antony");

$address = new Address();
$address->setCity("New York");

$user->setAddress($address);

$t = new ORM\Transaction($orm);
$t->persist($user);
$t->run();

The following SQL commands will be produced:

INSERT INTO "users" ("name") VALUES ('Antony');
INSERT INTO "addresses" ("city", "user_id") VALUES ('New York', 15);

You can also store objects separately, the ORM will automatically link them together:

$t = new ORM\Transaction($orm);
$t->persist($address);
$t->persist($user);
$t->run();

The generated command chain will automatically be sorted to keep the proper order of SQL operations.

Retrieve the related entity

Though Cycle ORM support lazy loading using proxies (see extension cycle/proxy-factory), it is recommended to pre-load needed relations using custom repository methods.

To load related object use the load method of Cycle\ORM\Select. The relation can be loaded using property name:

$result = $orm->getRepository(User::class)
    ->select()
    ->load('address')
    ->fetchAll();

foreach ($result as $user) {
    print_r($user);
}

This will produce the SQL similar to:

SELECT
    "user"."id" AS "c0",
    "user"."name" AS "c1",
    "l_user_address"."id" AS "c2",
    "l_user_address"."city" AS "c3",
    "l_user_address"."user_id" AS "c4"
FROM "users" AS "user"
LEFT JOIN "addresses" AS "l_user_address"
    ON "l_user_address"."user_id" = "user"."id";

Please note, by default ORM will try to load a hasOne relation using LEFT JOIN. You can alter this behaviour and force loading using an external query (post load) by modifying the load method:

$result = $orm->getRepository(User::class)
    ->select()
    ->load('address', ['method' => Select::OUTER_QUERY])
    ->fetchAll();

foreach ($result as $user) {
    print_r($user);
}

In this case, the resulted SQL will look like:

SELECT
    "user"."id" AS "c0",
    "user"."name" AS "c1"
FROM "users" AS "user";
SELECT
    "user_address"."id" AS "c0",
    "user_address"."city" AS "c1",
    "user_address"."user_id" AS "c2"
FROM "addresses" AS "user_address"
WHERE "user_address"."user_id" IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);

You can load relation level using dot notation $select->load('post.comments.author')

Filter by relation

To filter the selection using related data use the method with. Once this method is invoked you can address the relation fields in the where method using the relation name as a prefix:

$result = $orm->getRepository(User::class)
    ->select()
    ->with('address')->where('address.city', 'New York')
    ->fetchAll();

The SQL:

SELECT
    "user"."id" AS "c0",
    "user"."name" AS "c1",
    "user_address"."id" AS "c2",
    "user_address"."city" AS "c3",
    "user_address"."user_id" AS "c4"
FROM "users" AS "user"
INNER JOIN "addresses" AS "user_address"
    ON "user_address"."user_id" = "user"."id"
WHERE "user_address"."city" = 'New York';

You can freely combine load and with method, ORM will help you to avoid collisions:

$result = $orm->getRepository(User::class)
    ->select()
    ->with('address')->where('address.city', 'New York')
    ->load('address')
    ->fetchAll();

And the resulted SQL:

SELECT
    "user"."id" AS "c0",
    "user"."name" AS "c1",
    "l_user_address"."id" AS "c2",
    "l_user_address"."city" AS "c3",
    "l_user_address"."user_id" AS "c4",
    "user_address"."id" AS "c5",
    "user_address"."city" AS "c6",
    "user_address"."user_id" AS "c7"
FROM "users" AS "user"
LEFT JOIN "addresses" AS "l_user_address"
    ON "l_user_address"."user_id" = "user"."id"
INNER JOIN "addresses" AS "user_address"
    ON "user_address"."user_id" = "user"."id"
WHERE "user_address"."city" = 'New York';

You can also force select to use one JOIN for both load and with methods via using option:

$result = $orm->getRepository(User::class)
    ->select()
    ->with('address', ['as' => 'user_address'])->where('address.city', 'New York')
    ->load('address', ['using' => 'user_address'])
    ->fetchAll();

In this case, only one JOIN will be produced:

SELECT
    "user"."id" AS "c0",
    "user"."name" AS "c1",
    "user_address"."id" AS "c2",
    "user_address"."city" AS "c3",
    "user_address"."user_id" AS "c4",
    "user_address"."id" AS "c5",
    "user_address"."city" AS "c6",
    "user_address"."user_id" AS "c7"
FROM "users" AS "user"
INNER JOIN "addresses" AS "user_address"
    ON "user_address"."user_id" = "user"."id"
WHERE "user_address"."city" = 'New York';

Combined Selections

The strict separation between load and with methods grants you the ability to control filter and load scope separately. For example, to find a user with any published post and load all user posts with all visible comments:

$users = $orm->getRepository(User::class)
    ->distinct() // required due join of posts
    ->with('posts')->where('posts.published', true)
    ->load('posts.comments', ['where' => ['visible' => true]])
    ->fetchAll();
Edit this page