diff --git a/config.json b/config.json index 2605b67a9db..31a71be53ff 100644 --- a/config.json +++ b/config.json @@ -153,6 +153,10 @@ "type": "bool", "default": true }, + "orm.session_cache": { + "type": "bool", + "default": false + }, "warning.enable": { "type": "bool", "default": true diff --git a/phalcon/Mvc/Model.zep b/phalcon/Mvc/Model.zep index e5f5bf9b9ac..5d3b65f5c75 100644 --- a/phalcon/Mvc/Model.zep +++ b/phalcon/Mvc/Model.zep @@ -163,6 +163,11 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, */ protected uniqueTypes = []; + /** + * @var string + */ + protected modelUUID; + /** * Phalcon\Mvc\Model constructor */ @@ -5992,4 +5997,39 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, } } } -} + + + /** + * set the model UUID for session cache + * + * @var string uuid + * @return void + */ + public function setModelUUID(string uuid) -> void + { + let this->modelUUID = uuid; + } + + /** + * get the model UUID for session cache + * + * @return string + */ + public function getModelUUID() -> string + { + return this->modelUUID; + } + + /** + * Used to destroy reference in WeakCache + * + * @return void + */ + public function __destruct() + { + if true === globals_get("orm.session_cache") { + this->modelsManager->getSessionCache()->delete(this->modelUUID); + } + } + +} \ No newline at end of file diff --git a/phalcon/Mvc/Model/Manager.zep b/phalcon/Mvc/Model/Manager.zep index 99cee971aef..115d402ae2c 100644 --- a/phalcon/Mvc/Model/Manager.zep +++ b/phalcon/Mvc/Model/Manager.zep @@ -19,6 +19,7 @@ use Phalcon\Mvc\ModelInterface; use Phalcon\Mvc\Model\Query\Builder; use Phalcon\Mvc\Model\Query\BuilderInterface; use Phalcon\Mvc\Model\Query\StatusInterface; +use Phalcon\Storage\Adapter\AbstractAdapter; use ReflectionClass; use ReflectionProperty; @@ -221,6 +222,13 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ protected reusable = []; + /** + * Thread cache. + * + * @var AbstractAdapter|null + */ + protected sessionCache = null; + /** * Destroys the current PHQL cache */ @@ -2377,4 +2385,30 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI return isset this->{collection}[keyRelation]; } + + /** + * Sets a cache for model working in memory + */ + public function hasSessionCache() -> bool + { + return this->sessionCache !== null; + } + + + /** + * Sets a cache for model working in memory + */ + public function setSessionCache( cache) -> void + { + let this->sessionCache = cache; + } + + /** + * Returns a cache instance or null if not configured + */ + public function getSessionCache() -> | null + { + return this->sessionCache; + } + } diff --git a/phalcon/Mvc/Model/MetaData.zep b/phalcon/Mvc/Model/MetaData.zep index 554c48b8774..a9d476d7a48 100644 --- a/phalcon/Mvc/Model/MetaData.zep +++ b/phalcon/Mvc/Model/MetaData.zep @@ -924,8 +924,8 @@ abstract class MetaData implements InjectionAwareInterface, MetaDataInterface * * @return string */ - public final function getColumnMapUniqueKey( model) -> string | null - { + public final function getColumnMapUniqueKey( model) -> string | null + { string key; let key = get_class_lower(model); if false === isset(this->columnMap[key]) { @@ -934,5 +934,32 @@ abstract class MetaData implements InjectionAwareInterface, MetaDataInterface } } return key; - } + } + + /** + * Returns the model UniqueID based on model and array row primary key(s) value(s) + */ + public function getModelUUID( model, array row) -> string | null + { + var pk, pks; + string uuid; + let pks = this->readMetaDataIndex(model, self::MODELS_PRIMARY_KEY); + if null === pks { + return null; + } + let uuid = get_class(model); + + for pk in pks { + let uuid = uuid . ":" . row[pk]; + } + return uuid; + } + + /** + * Compares if two models are the same in memory + */ + public function modelEquals( first, other) -> bool + { + return spl_object_id(first) === spl_object_id(other); + } } diff --git a/phalcon/Mvc/Model/Query.zep b/phalcon/Mvc/Model/Query.zep index 9085f5cb52f..99418ca006c 100644 --- a/phalcon/Mvc/Model/Query.zep +++ b/phalcon/Mvc/Model/Query.zep @@ -1334,7 +1334,9 @@ class Query implements QueryInterface, InjectionAwareInterface resultObject, resultData, cache, - isKeepingSnapshots + isKeepingSnapshots, + manager, + metaData ); } @@ -1344,7 +1346,9 @@ class Query implements QueryInterface, InjectionAwareInterface return new Complex( columns1, resultData, - cache + cache, + manager, + metaData ); } diff --git a/phalcon/Mvc/Model/Resultset.zep b/phalcon/Mvc/Model/Resultset.zep index b53ed2fb33b..8f6deacee9e 100644 --- a/phalcon/Mvc/Model/Resultset.zep +++ b/phalcon/Mvc/Model/Resultset.zep @@ -126,13 +126,31 @@ abstract class Resultset */ protected result; + /** + * @var \Phalcon\Mvc\Model\Manager|null + */ + protected manager = null; + + + /** + * @var \Phalcon\Mvc\Model\Metadata|null + */ + protected metaData = null; + + /** + * Thread cache. + * + * @var \Phalcon\Session\Adapter\AbstractAdapter|null + */ + protected sessionCache = null; + /** * Phalcon\Mvc\Model\Resultset constructor * * @param ResultInterface|false $result * @param mixed|null $cache */ - public function __construct(var result, var cache = null) + public function __construct(var result, var cache = null, manager = null, metaData = null) { var prefetchRecords, rowCount, rows; @@ -145,7 +163,15 @@ abstract class Resultset return; } - + if true === globals_get("orm.session_cache") { + if null !== manager { + let this->manager = manager; + let this->sessionCache = manager->getSessionCache(); + } + if null !== metaData { + let this->metaData = metaData; + } + } /** * Valid resultsets are Phalcon\Db\ResultInterface instances */ diff --git a/phalcon/Mvc/Model/Resultset/Complex.zep b/phalcon/Mvc/Model/Resultset/Complex.zep index dfc4cda31d7..bd48fd9f5f5 100644 --- a/phalcon/Mvc/Model/Resultset/Complex.zep +++ b/phalcon/Mvc/Model/Resultset/Complex.zep @@ -53,7 +53,9 @@ class Complex extends Resultset implements ResultsetInterface public function __construct( var columnTypes, result = null, - var cache = null + var cache = null, + manager = null, + metaData = null ) { /** @@ -61,7 +63,7 @@ class Complex extends Resultset implements ResultsetInterface */ let this->columnTypes = columnTypes; - parent::__construct(result, cache); + parent::__construct(result, cache, manager, metaData); } /** @@ -71,7 +73,7 @@ class Complex extends Resultset implements ResultsetInterface { var row, hydrateMode, eager, dirtyState, alias, activeRow, type, column, columnValue, value, attribute, source, attributes, columnMap, - rowModel, keepSnapshots, sqlAlias, modelName; + rowModel, keepSnapshots, sqlAlias, modelName, uuid, model; let activeRow = this->activeRow; @@ -170,35 +172,50 @@ class Complex extends Resultset implements ResultsetInterface if !fetch keepSnapshots, column["keepSnapshots"] { let keepSnapshots = false; } + /** + * checks for session cache and returns already in memory models + */ + let value = null; + if true === globals_get("orm.session_cache") { + let modelName = get_class(column["instance"]); + let model = new {modelName}(); + let uuid = this->metaData->getModelUUID(model, row); + let value = this->sessionCache->get(uuid); + } - if globals_get("orm.late_state_binding") { - if column["instance"] instanceof Model { - let modelName = get_class(column["instance"]); + if null === value { + if globals_get("orm.late_state_binding") { + if column["instance"] instanceof Model { + let modelName = get_class(column["instance"]); + } else { + let modelName = "Phalcon\\Mvc\\Model"; + } + + let value = {modelName}::cloneResultMap( + column["instance"], + rowModel, + columnMap, + dirtyState, + keepSnapshots + ); } else { - let modelName = "Phalcon\\Mvc\\Model"; + /** + * Get the base instance. Assign the values to the + * attributes using a column map + */ + let value = Model::cloneResultMap( + column["instance"], + rowModel, + columnMap, + dirtyState, + keepSnapshots + ); + } + if true === globals_get("orm.session_cache") { + this->sessionCache->set(uuid, value); + value->setModelUUID(uuid); } - - let value = {modelName}::cloneResultMap( - column["instance"], - rowModel, - columnMap, - dirtyState, - keepSnapshots - ); - } else { - /** - * Get the base instance. Assign the values to the - * attributes using a column map - */ - let value = Model::cloneResultMap( - column["instance"], - rowModel, - columnMap, - dirtyState, - keepSnapshots - ); } - break; default: diff --git a/phalcon/Mvc/Model/Resultset/Simple.zep b/phalcon/Mvc/Model/Resultset/Simple.zep index 320ab145da5..2b7a34fc091 100644 --- a/phalcon/Mvc/Model/Resultset/Simple.zep +++ b/phalcon/Mvc/Model/Resultset/Simple.zep @@ -56,7 +56,9 @@ class Simple extends Resultset var model, result, var cache = null, - bool keepSnapshots = false + bool keepSnapshots = false, + manager = null, + metaData = null ) { let this->model = model, @@ -66,7 +68,7 @@ class Simple extends Resultset */ let this->keepSnapshots = keepSnapshots; - parent::__construct(result, cache); + parent::__construct(result, cache, manager, metaData); } /** @@ -74,7 +76,7 @@ class Simple extends Resultset */ final public function current() -> | null { - var row, hydrateMode, columnMap, activeRow, modelName; + var row, hydrateMode, columnMap, activeRow, modelName, uuid; let activeRow = this->activeRow; @@ -111,6 +113,18 @@ class Simple extends Resultset */ switch hydrateMode { case Resultset::HYDRATE_RECORDS: + /** + * checks for session cache and returns already in memory models + */ + if true === globals_get("orm.session_cache") { + let uuid = this->metaData->getModelUUID(this->model, row); + let activeRow = this->sessionCache->get(uuid); + if null !== activeRow { + let this->activeRow = activeRow; + return activeRow; + } + } + /** * Set records as dirty state PERSISTENT by default * Performs the standard hydration based on objects @@ -138,7 +152,10 @@ class Simple extends Resultset this->keepSnapshots ); } - + if true === globals_get("orm.session_cache") { + this->sessionCache->set(uuid, activeRow); + activeRow->setModelUUID(uuid); + } break; default: diff --git a/tests/_data/fixtures/Migrations/OrdersMigration.php b/tests/_data/fixtures/Migrations/OrdersMigration.php index b9dbe6a7a38..5a2e5c06635 100644 --- a/tests/_data/fixtures/Migrations/OrdersMigration.php +++ b/tests/_data/fixtures/Migrations/OrdersMigration.php @@ -36,7 +36,7 @@ public function insert( insert into co_orders ( ord_id, ord_name ) values ( - {$ord_id}, {$ord_name} + {$ord_id}, '{$ord_name}' ) SQL; diff --git a/tests/_data/fixtures/Migrations/OrdersProductsMigration.php b/tests/_data/fixtures/Migrations/OrdersProductsMigration.php index a90def45bdf..68f27094c10 100644 --- a/tests/_data/fixtures/Migrations/OrdersProductsMigration.php +++ b/tests/_data/fixtures/Migrations/OrdersProductsMigration.php @@ -18,7 +18,7 @@ */ class OrdersProductsMigration extends AbstractMigration { - protected $table = "co_orders_x_products"; + protected $table = "private.co_orders_x_products"; /** * @param int $oxp_ord_id @@ -35,7 +35,7 @@ public function insert( $oxp_prd_id = $oxp_prd_id ?: 'null'; $oxp_quantity = $oxp_quantity ?: 'null'; $sql = << + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Tests\Database\Mvc\Model; + +use DatabaseTester; +use PDO; +use Phalcon\Cache\AdapterFactory; +use Phalcon\Cache\Cache; +use Phalcon\Mvc\Model\Exception; +use Phalcon\Storage\Adapter\Weak; +use Phalcon\Storage\SerializerFactory; +use Phalcon\Tests\Fixtures\Migrations\CustomersMigration; +use Phalcon\Tests\Fixtures\Migrations\InvoicesMigration; +use Phalcon\Tests\Fixtures\Migrations\ObjectsMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersProductsMigration; +use Phalcon\Tests\Fixtures\Migrations\ProductsMigration; +use Phalcon\Tests\Fixtures\Traits\DiTrait; +use Phalcon\Tests\Models\Customers; +use Phalcon\Tests\Models\Invoices; +use Phalcon\Tests\Models\Objects; +use Phalcon\Tests\Models\Orders; +use Phalcon\Tests\Models\Products; + +use function getOptionsRedis; +use function outputDir; +use function uniqid; + +/** + * Class FindCest + */ +class SessionCacheCest +{ + use DiTrait; + + public function _before(DatabaseTester $I) + { + $this->setNewFactoryDefault(); + $this->setDatabase($I); + ini_set('phalcon.orm.session_cache', '1'); + $modelsManager = $this->container->get('modelsManager'); + $cache = new Weak(new SerializerFactory()); + $modelsManager->setSessionCache($cache); + } + + public function _after(DatabaseTester $I) + { + $this->container['db']->close(); + ini_set('phalcon.orm.session_cache', '0'); + $modelsManager = $this->container->get('modelsManager'); + $cache = new Weak(new SerializerFactory()); + $modelsManager->setSessionCache($cache); + } + + /** + * Tests Phalcon\Mvc\Model :: find() + * + * @author Phalcon Team + * @since 2023-08-23 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelFind(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - find()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + $migration = new ObjectsMigration($connection); + $migration->insert(1, 'random data', 1); + + $data = Objects::find(); + + $I->assertEquals(1, count($data)); + + $record = $data[0]; + $I->assertEquals(1, $record->obj_id); + $I->assertEquals('random data', $record->obj_name); + + $other = Objects::findFirst(); + + $expected = spl_object_id($record); + $actual = spl_object_id($other); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: SessionCache Complex + * + * @author Phalcon Team + * @since 2023-08-23 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelRelationsComplex(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - SessionCache Complex'); + + + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $productId = 20; + $productName = uniqid('prd', true); + $quantity = 1; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsMigration = new OrdersProductsMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName); + $productsMigrations->insert($productId, $productName); + $ordersProductsMigration->insert($orderId, $productId, $quantity, 0, 0); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productsMigrations->insert($productId, $productName); + $ordersProductsMigration->insert($orderId, $productId, $quantity); + + + $order1 = Orders::findFirst(10); + + $products = $order1->products; + $expected = 2; + $actual = count($products); + $I->assertEquals($expected, $actual); + + $productRelation = $products->getFirst(); + $productFind = Products::findFirst($productRelation->prd_id); + $expected = spl_object_id($productRelation); + $actual = spl_object_id($productFind); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: SessionCache simple + * + * @author Phalcon Team + * @since 2023-08-23 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelRelationsSimple(DatabaseTester $I) + { + + $I->wantToTest('Mvc\Model - SessionCache Simple'); + + $connection = $I->getConnection(); + + $custIdOne = 50; + $firstNameOne = uniqid('cust-1-', true); + $lastNameOne = uniqid('cust-1-', true); + + $customersMigration = new CustomersMigration($connection); + $customersMigration->insert($custIdOne, 0, $firstNameOne, $lastNameOne); + + $invoiceId = 50; + $title = uniqid('inv-'); + $invoicesMigration = new InvoicesMigration($connection); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + Invoices::STATUS_PAID, + $title . '-paid' + ); + $invoiceId = 70; + $title = uniqid('inv-'); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + 0, + $title . '' + ); + + $customer1 = Customers::findFirst(50); + + $invoices = $customer1->getRelated('invoices'); + $actual = count($invoices); + $expected = 2; + $I->assertEquals($expected, $actual); + + $invoice1 = $invoices->getFirst(); + $invoice2 = Invoices::findFirst($invoice1->inv_id); + $expected = spl_object_id($invoice1); + $actual = spl_object_id($invoice2); + $I->assertEquals($expected, $actual); + + $customer2 = $invoice1->customer; + $expected = spl_object_id($customer1); + $actual = spl_object_id($customer2); + $I->assertEquals($expected, $actual); + } +}