Giter VIP home page Giter VIP logo

cakephp-lazyload's Introduction

Packagist license

CakePHP ORM LazyLoad Plugin

This is an association lazy loader for CakePHP ORM entities. It allows you to lazily load association data by accessessing the property, without using contain() (the eager loader).

Installation

Requirements

  • CakePHP ORM (or the full framework) 5.x
  • sloth

$ composer require jeremyharris/cakephp-lazyload

For older versions of the CakePHP ORM, look at older releases of this library.

Usage

If you have a base entity, add the trait to get lazy loading across all of your entities. Or, attach it to a single entity to only lazy load on that entity:

src/Model/Entity/User.php

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use JeremyHarris\LazyLoad\ORM\LazyLoadEntityTrait;

class User extends Entity
{
    use LazyLoadEntityTrait;
}

Associations to the Users table can now be lazily loaded from the entity!

Example

Let's assume that our base entity has the LazyLoadEntityTrait and:

Brewery hasMany Beers
Programmer belongsToMany Beers

With the lazy loader, all we need is the entity:

<?php
// get an entity, don't worry about contain
$programmer = $this->Programmers->get(1);

When accessing an association property (as if the data was eagerly loaded), the associated data is loaded automatically.

<?php
// beers is lazy loaded
foreach ($programmer->beers as $beer) {
    // brewery is lazy loaded onto $beer
    echo $programmer->name . ' drinks beer ' .  $beer->name . ' from ' . $beer->brewery->name;
}

Using contain in conjunction with the lazy loader

The lazy loader will not overwrite results that are generated by the eager loader (contain()). You can continue to write complex contain conditions and still take advantage of the lazy loader.

<?php
$programmer = $this->Programmers->get(1, [
    'contain' => [
        'Beers'
    ]
]);

// beers is loaded via the eager loader
$programmer->beers;
// brewery is lazy loaded onto $beer[0]
$programmer->beers[0]->brewery;

Entity method support

Entities with the lazy loader trait support lazy loading using the different property access methods provided by the Cake ORM:

  • Getters: $programmer->get('beers), $programmer->beers
  • Has: $programmer->has('beers)
  • Unset: $programmer->unsetProperty('beers')

When unsetting a property via Entity::unsetProperty(), the property will be prevented from being lazily loaded in the future for that entity, as it respects the state in the same way a typical Entity would. If you wish to re-hydrate an association, you can use Table::loadInto as provided by the ORM:

<?php
$programmer = $this->Programmers->get(1);

// beers is lazy loaded
$programmer->beers;
// remove beers from the entity
$programmer->unsetProperty('beers');
// this now returns false
$programmer->has('beers');
// if we want access to beers again, we can manually load it
$programmer = $this->Programmers->loadInto($programmer, ['Beers']);

Testing

Sometimes in tests, we create entities that don't necessarily have tables. When accessing a property that doesn't exist, the LazyLoad trait will try to load the table in order to get association data, which would throw an error if the table doesn't exist. To prevent this, you can override _repository() in your entity:

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Exception;
use JeremyHarris\LazyLoad\ORM\LazyLoadEntityTrait;

class User extends Entity
{
    use LazyLoadEntityTrait {
        _repository as _loadRepository;
    }

    protected function _repository()
    {
        try {
            $repository = $this->_loadRepository();
        } catch (Exception $e) {
            return false;
        }
        return $repository;
    }
}

By default, the LazyLoad trait will throw whatever error bubbles up TableRegistry::get().

Plugins

If testing plugins entities that don't have tables, make sure to override the _repository() method to return the plugin's table.

Notes

  • Contain: This is not a replacement for contain(), which can write complex queries to dictate what data to contain. The lazy loader obeys the association's conditions that you set when defining the association on the table, but apart from that it grabs all associated data.
  • Speed: Lazy loading in this manner isn't necessarily a speed improvement. In fact, it can be a detriment to speed in certain cases, such as looping over entities that lazy load associations within the loop (creates a single SQL query per-item rather than using joins or the ORM). This plugin is intended as a helper for bootstrapping projects.
  • Hydration: The lazy loader requires that your result set is hydrated in order to provide lazy loading functionality.

Special thanks to @lorenzo for reviewing the plugin before its initial release!

cakephp-lazyload's People

Contributors

fabiofdsantos avatar jeremyharris avatar lorenzo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cakephp-lazyload's Issues

No support for get() method

A trait seems to be overloading __get() magic method which is just a wrapper for get() method.

So... lazy loading through $entity->property works but it doesn't work with $entity->get('property') which is weird.

I think this should be fixed unless it is done like that intentionally (although I couldn't figure out why).

Useless queries when primary key is unset

It generates useless queries to the database.

For example, if Orders.id is unset:

SELECT ...
FROM orders Orders 
LEFT JOIN delivery_orders DeliveryOrders ON DeliveryOrders.id = (Orders.delivery_order_id) 
WHERE (Orders.id IN (NULL) AND Orders.deleted IS NULL)

LazyLoadTrait does not respect unsetting a property [part II]

Seems like #9 hasn't been fixed.

Consider this example again. This time group has been eager-loaded.

$user = $users->find()->first();
$user->get('group');
$user->unsetProperty('group');
$users->save($user, ['associated' => 'Groups']); 

_lazyLoaded hasn't noted that there's group already inside an entity. Now I unset it and lazy loader loads it again.

Maybe _lazyLoaded should be notified that a property has been unset?

This issue causes TranslateBehavior association loading to be stuck in a loop as _rowMapper unset eagerly loaded association properties but lazy loader wants to load them again...

Here is my temporary fix:

    public function unsetProperty($property)
    {
        $this->_lazyLoaded[] = $property;

        return parent::unsetProperty($property);
    }

But maybe it could be done in a more elegant way.

4.0.2 not backwards compatible with 4.0.1

I just updated to 4.0.2 and several of my unit tests failed. Reverting to 4.0.1 fixed the issue.
I've so far managed to track the failure down to resolving a belongsTo association

Context

  • There's Issue and User, where Issue belongsTo User via Author and Issue BelongsTo User via Assignee
    • Issue.issue_id
    • Issue.author_id
    • Issue.assignee_id
    • User.user_id
  • I then try to lazy load author ($issue->author)
  • $this->get($association->getBindingKey())
  • $this->get($association->getBindingKey()) (which seems to be the change from 4.0.1 breaking things for me

Possible solution

Probably this is me not grasping something, but in the check:
$this->get($association->getBindingKey()) === null (LazyLoadEntityTrait line 131)
why is $association->getBindingKey() (i.e. user_id) gotten on the object we are lazy loading into (an instance of issue). A check of $this->get($association->getForeignKey()) === null seems to make more sense to me?

Apologies for this very "vague" issue, but felt I had to post it anyway. I'm not very familiar with these types of submissions, so feel free to give my tips/pointers/suggestions for improvements.

LazyLoadTrait does not respect unsetting a property.

Consider following example:

Users belongsTo Groups

$user = $users->find()->first(); //$user does not have a group loaded but uses LazyLoadTrait
$user->get('group'); //this will load a group into user
$user->unsetProperty('group');
$user->get('group') //this will load a group again

It could seem like a desired behaviour but consider this:

$user = $users->find()->first();
$user->get('group');
$user->unsetProperty('group');
$users->save($user, ['associated' => 'Groups']); 

I expect that my Groups association will be removed from a DB. But lazy loader will load a group once again before saving an entity. I tried also unsetting a group_id property $user->unsetProperty('group_id'); but this fails too.

I think that a dirty() check could fix the issue.

Manually unsetting a property from an entity does not lazy load it again

I've updated from 1.0.2 to 1.0.3 and after that all my tests started crashing because there's a moment in my code where I do the following:

// unset any possible previously related center
unset($member->center);

I do this because this is a signup process where the center is related to the member (and I need it that way), but if the member goes back and chooses another center i need to repopulate the $member->center object with the appropriate data.

The thing is that upgrading to 1.0.3 started throwing errors because after the unset($member->center) the center entity does not get populated again.

I've no idea if this is related to #12 or to the 1.0.3 change:

Fixes lazily loading a previously unset property that was eagerly loaded

But the thing is I cannot update from 1.0.2 to 1.0.3 as it's a critical change for my app.

Using CakePHP 3.3.15

PS. If you need any other info, please, tell me. I'm opening this issue as fast as I can because I'm in the final step of this week's sprint.

By-reference array functions don't work with LazyLoadableEntity entities

Example here: https://github.com/themrwilliams/cakephp-lazyload/commit/185aa5a4befb3b4a3e016ea79d5bb9a6894d4c2d

Both tests are the same scenario. The first one, with the trait disabled, passes. The second one fails.

I plan to dig into it more when I have time, but opening this for now in case someone has already run into a similar issue before.

EDIT: This does seem to apply to any of the by-reference array functions (e.g. array_pop, etc.).

Composite foreign keys

The issue

This trait is currently missing support for foreign keys that consist out of multiple columns.

The cause

The suspected line of code is at line 133:

$isMissingBelongsToFK = $association instanceof BelongsTo && !isset($this->_fields[$association->getForeignKey()]);

The code fails to check the return type of the getForeignKey() call, which is defined as string|string[]. If the foreign key is defined as an array of columns, the trait will fail with:

Response: {\n
    "message": "Illegal offset type in isset or empty",\n
    "exception": "TypeError",\n
    "file": "/var/www/html/vendor/jeremyharris/cakephp-lazyload/src/ORM/LazyLoadEntityTrait.php",\n
    "line": 133,\n
    "trace": [\n
        {\n
            "file": "/var/www/html/vendor/jeremyharris/cakephp-lazyload/src/ORM/LazyLoadEntityTrait.php",\n
            "line": 39,\n
            "function": "_lazyLoad",\n
            "class": "App\\Model\\Entity\\MyModel",\n
            "type": "->"\n
        },\n
        {\n
            "file": "/var/www/html/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php",\n
            "line": 129,\n
            "function": "get",\n
            "class": "App\\Model\\Entity\\MyModel",\n
            "type": "->"\n
        },\n
...

What happens here is that you try to access an array index of type array, which can't work.

The solution

$isMissingBelongsToFK = false;
if ($association instanceof BelongsTo) {
    $foreignKeys = $association->getForeignKey();
    if (is_array($foreignKeys)) {
        foreach ($foreignKeys as $foreignKey) {
            if (!isset($this->_fields[$foreignKey])) {
                $isMissingBelongsToFK = true;
                break;
            }
        }
    } else {
        $isMissingBelongsToFK = !isset($this->_fields[$foreignKeys]);
    }
}

Replace travis with github actions

travis-ci.org is no longer running. We should migrate to github actions for CI. If someone is willing to create a PR for this I would really appreciate it. Not sure when I can get to it.

Issues when association is null

Context:
I have EntityA which hasOne EntityB that can be NULL.

My problem:
I'm trying to cache the association to avoid querying it everytime i call $entityA->entity_b, so on the EntityA's Entity file i have:

	public function _getEntityB()
	{
		if ($this->entityB === 'UNSET')
			$this->entityB = TableRegistry::get('EntityB')->findByKeyId($this->id)->first();
		
		return $this->entityB;
	}

but because entityB is NULL, _parentGet returns null and tries to lazyload it everytime.

Is there any solution besides changing $entityA->entity_b for something else ?

Trying to lazy load on new entities

Is there a good reason why LazyLoadEntityTrait::_lazyLoad doesn't short-circuit and return null immediately if $this->isNew()? My use case where this causes problems is perhaps a bit obscure, but I don't see an obvious downside to including this test.

[bug] Infinite loop check for nullable associations

Lazy load seems to be stuck in a loop while loading nullable belongsTo associates.
I think that lazy loader should check if a property has been already set to null.

For example:

protected function _lazyLoad($property)
{
    if (array_key_exists($property, $this->_properties)) {
        return $this->_properties[$property];
    }
    ...
}

This might fix #9

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.