{"id":18477814,"url":"https://github.com/ikkez/f3-cortex","last_synced_at":"2025-05-16T02:10:16.717Z","repository":{"id":775898,"uuid":"35862066","full_name":"ikkez/f3-cortex","owner":"ikkez","description":"A multi-engine ORM / ODM for the PHP Fat-Free Framework","archived":false,"fork":false,"pushed_at":"2025-02-26T21:39:01.000Z","size":774,"stargazers_count":121,"open_issues_count":23,"forks_count":22,"subscribers_count":24,"default_branch":"master","last_synced_at":"2025-05-11T18:05:45.787Z","etag":null,"topics":["data-mapper","fat-free-framework","filedb","mongodb","orm","php","sql"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ikkez.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.txt","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"ikkez","buy_me_a_coffee":"ikkez"}},"created_at":"2015-05-19T05:52:37.000Z","updated_at":"2025-05-02T12:28:18.000Z","dependencies_parsed_at":"2024-06-18T14:06:44.632Z","dependency_job_id":"b4c2f058-0835-4c39-bfd0-1d10733b6af6","html_url":"https://github.com/ikkez/f3-cortex","commit_stats":{"total_commits":499,"total_committers":9,"mean_commits":55.44444444444444,"dds":"0.10420841683366733","last_synced_commit":"44b30fd4ca1b199729778d72f3840318cc64dfd9"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ikkez%2Ff3-cortex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ikkez%2Ff3-cortex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ikkez%2Ff3-cortex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ikkez%2Ff3-cortex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ikkez","download_url":"https://codeload.github.com/ikkez/f3-cortex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254453667,"owners_count":22073618,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-mapper","fat-free-framework","filedb","mongodb","orm","php","sql"],"created_at":"2024-11-06T12:06:16.376Z","updated_at":"2025-05-16T02:10:16.700Z","avatar_url":"https://github.com/ikkez.png","language":"PHP","readme":"![Cortex](https://ikkez.de/linked/cortex_icon.png)\n***\n\n### A general purpose Data-Mapper for the PHP Fat-Free Framework\n\n[![Latest Stable Version](https://poser.pugx.org/ikkez/f3-cortex/v)](https://packagist.org/packages/ikkez/f3-cortex)\n[![Total Downloads](https://poser.pugx.org/ikkez/f3-cortex/downloads)](https://packagist.org/packages/ikkez/f3-cortex)\n\nCortex is a multi-engine ActiveRecord ORM / ODM that offers easy object persistence. Some of its main features are:\n\n  - It handles SQL, Jig and MongoDB database engines\n  - Write queries in well-known SQL Syntax, they can be translated to Jig and Mongo\n  - automated SQL table creation and column extension from defined schema configurations\n  - Easy prototyping with the SQL Fluid Mode, which makes your RDBMS schema-less and adds new table columns automatically\n  - Support for models and collections\n  - Relationships: link multiple models together to one-to-one, one-to-many and many-to-many associations\n  - smart-loading of related models (intelligent lazy and eager-loading with zero configuration)\n  - useful methods for nested filtering through relations \n  - lots of event handlers and custom setter / getter preprocessors for all fields\n  - define default values and nullable fields for NoSQL\n  - additional [validation plugin](https://github.com/ikkez/f3-validation-engine) available \n\nWith Cortex you can create generic apps, that work with any DB of the users choice, no matter if it's SQlite, PostgreSQL, MongoDB or even none.\nYou can also mash-up multiple engines or use them simultaneously.\n\nIt's great for fast and easy data abstraction and offers a bunch of useful filter possibilities.\n\n\n---\n\n## Table of Contents\n\n1. [Quick Start](#quick-start)\n2. [SQL Fluid Mode](#sql-fluid-mode)\n3. [Cortex Models](#cortex-models)\n\t1. [Configuration](#configuration)\n\t\t1. [Additional Data Types](#additional-data-types)\n\t\t2. [Alternative Configuration](#alternative-configuration-method)\n\t\t2. [Blacklist Fields](#blacklist-fields)\n\t2. [Setup](#set-up)\n\t3. [Setdown](#set-down)\n4. [Relations](#relations)\n\t1. [Setup the linkage](#setup-the-linkage)\n\t2. [Working with Relations](#working-with-relations)\n\t\t1. [One-To-One](#one-to-one)\n\t\t2. [Many-To-Many, bidirectional](#many-to-many-bidirectional)\n\t\t3. [Many-To-Many, unidirectional](#many-to-many-unidirectional)\n\t\t4. [Many-To-Many, self-referencing](#many-to-many-self-referencing)\n5. [Event Handlers](#event-handlers)\n\t1. [Custom Field Handler](#custom-field-handler)\n6. [Filter Query Syntax](#filter-query-syntax)\n\t1. [Operators](#operators)\n\t2. [Options Array](#options)\n7. [Advanced Filter Techniques](#advanced-filter-techniques)\n\t1. [has](#has)\n\t2. [orHas](#orhas)\n\t3. [filter](#filter)\n8. [Insight into aggregation](#insight-into-aggregation)\n\t1. [Counting Relations](#counting-relations)\n\t2. [Virtual Fields](#virtual-fields)\n9. [Mapper API](#mapper-api)\n9. [Collection API](#collection-api)\n10. [Additional notes](#additional-notes)\n11. [Known Issues](#known-issues)\n12. [Roadmap](#roadmap)\n13. [License](#license)\n    \n\n## Quick Start\n\n### System Requirements\n\nCortex requires at least Fat-Free v3.4 and PHP 5.4. For some of the features, it also requires the F3 [SQL Schema Plugin](https://github.com/ikkez/f3-schema-builder/tree/master).\n\n### Install\n\nTo install Cortex, just copy the `/lib/db/cortex.php` file into your libs. For the SQL Schema Plugin, copy `lib/db/sql/schema.php` as well.\n\nIf you use **composer**, all you need is to run `composer require ikkez/f3-cortex:1.*` and it'll include Cortex and its dependencies into your package.\n\n### Setup a DB\n\nCreate a DB object of your choice. You can choose between [SQL](http://fatfreeframework.com/sql), [Jig](http://fatfreeframework.com/jig) or [MongoDB](http://fatfreeframework.com/mongo). Here are some examples:\n\n```php\n// SQL - MySQL\n$db = new \\DB\\SQL('mysql:host=localhost;port=3306;dbname=MyAppDB','user','pw');\n// SQL - SQlite\n$db = new \\DB\\SQL('sqlite:db/database.sqlite');\n// SQL - PostgreSQL \n$db = new \\DB\\SQL('pgsql:host=localhost;dbname=MyAppDB','user','pw');\n// SQL - SQL Server \n$db = new \\DB\\SQL('sqlsrv:SERVER=LOCALHOST\\SQLEXPRESS2012;Database=MyAppDB','user','pw');\n// Jig\n$db = new \\DB\\Jig('data/');\n// Mongo\n$db = new \\DB\\Mongo('mongodb://localhost:27017','testdb');\n```\n\n### Let's get it rolling\n\nIf you are familiar with F3's own Data-Mappers, you already know all about the basic CRUD operations you can do with Cortex too. It implements the ActiveRecord [Cursor Class](http://fatfreeframework.com/cursor) with all its methods. So you can use Cortex as a **drop-in replacement** of the F3 mappers and it's basic usage will stay that simple:\n\n```php\n$user = new \\DB\\Cortex($db, 'users');\n$user-\u003ename = 'Jack Ripper';\n$user-\u003email = 'jacky@email.com';\n$user-\u003esave();\n```\n\nAlright, that wasn't very impressive. But now let's find this guy again:\n\n```php\n$user-\u003eload(['mail = ?','jacky@email.com']);\necho $user-\u003ename; // shouts out: Jack Ripper\n```\n\nAs you can see, the filter array is pure SQL syntax, that you would already use with the F3 SQL Mapper. In Cortex this will work with all 3 DB engines. Here is a little more complex `where` criteria:\n\n```php\n$user-\u003eload(['name like ? AND (deleted = 0 OR rights \u003e ?]', 'Jack%', 3));\n```\n\nNo need for complex criteria objects or confusing Mongo where-array constructions. It's just as simple as you're used to. Using a Jig DB will automatically translate that query into the appropriate Jig filter:\n\n```php\nArray (\n    [0] =\u003e (isset(@name) \u0026\u0026 preg_match(?,@name)) AND ( (isset(@deleted) \u0026\u0026 (@deleted = 0)) OR (isset(@rights) \u0026\u0026 @rights \u003e ?) )\n    [1] =\u003e /^Jack/\n    [2] =\u003e 3\n)\n```\n\nAnd for MongoDB it translates into this:\n\n```php\nArray (\n    [$and] =\u003e Array (\n        [0] =\u003e Array (\n            [name] =\u003e MongoRegex Object (\n                    [regex] =\u003e ^Jack\n        )   )\n        [1] =\u003e Array (\n            [$or] =\u003e Array (\n                [0] =\u003e Array (\n                    [deleted] =\u003e 0\n                )\n                [1] =\u003e Array (\n                    [rights] =\u003e Array (\n                        [$gt] =\u003e 3\n                    )\n)))))\n```\n\nYou can use all the fancy methods from Cursor, like `load`, `find`, `cast`, `next` or `prev`. More about filtering and all the other methods a little later.\n\n## SQL Fluid Mode\n\nWhen you are prototyping some new objects or just don't want to bother with a table schema, while using Cortex along with a SQL DB backend, you can enable the SQL Fluid Mode.\nThis way Cortex will create all necessary tables and columns automatically, so you can focus on writing your application code. It will try to guess the right data type, based on the given sample data. To enable the fluid mode, just pass a third argument to the object's constructor:\n\n```php\n$user = new \\DB\\Cortex($db, 'users', TRUE);\n$user-\u003ename = 'John';            // varchar(256)\n$user-\u003eage = 25;                 // integer\n$user-\u003eactive = true;            // boolean|tinyint\n$user-\u003elastlogin = '2013-08-28'; // date\n```\n\nThis way it also creates data types of datetime, float, text (when `strlen \u003e 255`) and double.\n\n**Notice:** The fluid mode disables the caching of the underlying SQL table schema. This could slightly impact on performance, so keep in mind to deactivate this when you're done. Furthermore keep in mind that you are not able to load or find any records from tables that are not existing - consider to create and save some sample data first, so Cortex can create the tables.\n\n\n## Cortex Models\n\nUsing the Cortex class directly is easy for some CRUD operations, but to enable some more advanced features, you'll need to wrap Cortex into a Model class like this:\n\n```php\n// file at app/model/user.php\nnamespace Model;\n\nclass User extends \\DB\\Cortex {\n  protected\n    $db = 'AppDB1',     // F3 hive key of a valid DB object\n    $table = 'users';   // the DB table to work on\n}\n```\n\nNow you can create your mapper object that easy:\n\n```php\n$user = new \\Model\\Users();\n```\n\nThis is the minimal model configuration. Cortex needs at least a working DB object. You can also pass this through the constructor (`new \\Model\\Users($db);`) and drop it in the setup.\n`$db` must be a string of a hive key, where the DB object is stored *OR* the DB object itself.\nIf no `$table` is provided, Cortex will use the class name as table name.\n\n### Configuration\n\nCortex does not need that much configuration. But at least it would be useful to have setup the field configuration.\nThis way it's able to follow a defined schema of your data entity and enables you to use some auto-installation routines (see [setup](#set-up)). It looks like this:\n\n```php\n// file at app/model/user.php\nnamespace Model;\n\nclass User extends \\DB\\Cortex {\n\n  protected\n    $fieldConf = [\n        'name' =\u003e [\n            'type' =\u003e 'VARCHAR256',\n            'nullable' =\u003e false,\n        ],\n        'mail' =\u003e [\n            'type' =\u003e 'VARCHAR128',\n            'index' =\u003e true,\n            'unique' =\u003e true,\n        ],\n        'website' =\u003e [\n            'type' =\u003e 'VARCHAR256'\n        ],\n        'rights_level' =\u003e [\n            'type' =\u003e 'TINYINT',\n            'default' =\u003e 3,\n        ],\n    ],\n    $db = 'DB',\n    $table = 'users',\n    $primary = 'id';    // name of the primary key (auto-created), default: id\n}\n```\n\nIn the `$fieldConf` array, you can set data types (`type`), `nullable` flags and `default` values for your columns. With `index` and `unique`, you can even setup an index for the columns. Doing so enables you to install new Models into your SQL database, adds some nullable validation checks and the ability for defaults to NoSQL engines. This makes your models easy interchangeable along various databases using this loosely coupled field definitions. \n\n**You don't need to configure all fields this way.** If you're working with existing tables, the underlying SQL Mapper exposes the existing table schema. So if you don't need that auto-installer feature, you can just skip the configuration for those fields, or just setup only those you need (i.e. for fields with relations).\n\nBecause column data types are currently only needed for setting up the tables in SQL, it follows that [SQL Data Types Table](https://github.com/ikkez/f3-schema-builder/tree/master#column-class) from the required [SQL Schema Plugin](https://github.com/ikkez/f3-schema-builder/blob/master/lib/db/sql/schema.php).\n\nYou may also extend this config array to have a place for own validation rules or whatever you like.\n\nThe data type values are defined constants from the Schema Plugin. If you like to use some auto-completion in your IDE to find the right values, type in the longer path to the constants:\n\n```php\n'type' =\u003e \\DB\\SQL\\Schema::DT_TIMESTAMP,\n'default' =\u003e \\DB\\SQL\\Schema::DF_CURRENT_TIMESTAMP,\n```\n\n#### Additional Data Types\n\nCortex comes with two own data types for handling array values in fields. Even when Jig and Mongo support them naturally, most SQL engines do not yet. Therefore Cortex introduces:\n\n+ `DT_SERIALIZED`\n+ `DT_JSON`\n\nIn example:\n\n```php\n'colors' =\u003e [\n    'type' =\u003e self::DT_JSON,\n],\n```\n\nNow you're able to save array data in your model field, which is json_encoded into a `text` field behind the scene (when using a SQL backend).\n\n```php\n$mapper-\u003ecolors = ['red','blue','green'];\n```\n\n\n#### Alternative Configuration\n\nIn case you need some more flexible configuration and don't want to hard-wire it, you can overload the Model class constructor to load its config from an `ini`-file or elsewhere. In example:\n\n```php\nclass User extends \\DB\\Cortex {\n\n    function __construct() {\n        // get the DB from elsewhere\n        $this-\u003edb = \\Registry::get('DB');\n        $f3 = \\Base::instance();\n        // load fields from .ini file\n        if (!$f3-\u003eexists('usermodel'))\n            $f3-\u003econfig('app/models/usermodel.ini');\n        foreach ($f3-\u003eget('usermodel') as $key =\u003e $val)\n            $this-\u003e{$key} = $val;\n        parent::__construct();\n    }\n}\n```\n\nAnd in your `usermodel.ini` file:\n\n``` ini\n[usermodel]\ntable = users\n\n[usermodel.fieldConf]\nname.type = VARCHAR256\nname.nullable = FALSE\nmail.type = VARCHAR128\nwebsite.type = VARCHAR256\nrights_level.type = TINYINT\nrights_level.default = 3\n```\n\n#### Blacklist Fields\n\nThe `fields()` method can be used to return the available fields on the current model. If called with one simple array argument like `$news-\u003efields(['title']);`, it'll apply the provided elements as a whitelist to the whole mapper. For the rest of its lifetime it'll only hydrate the fields you permitted here.\nIf called with a 2nd argument like `$news-\u003efields(['author']),true);`, the array is going to be uses as a blacklist instead, and restrict the access to the provided fields.\nYou can also define deep nested fields using a **dot** as separator: `$news-\u003efields(['tags.title']);` will only hydrate the tag title in your news model and wont load or save any other field that exists in your tag model. Subsequent calls to the `fields` method will merge with all already defined blacklist/whitelist definitions.\n\n\n### Set up\n\nThis method creates the SQL DB tables you need to run your Cortex model. **It also adds just missing fields to already existing tables.**\n\nIf your Model has a valid field configuration, you are able to run this installation method:\n\n```php\n\\Model\\User::setup();\n``` \n\nIf you have no model class, you need to provide all of the setup method's parameters.\n\n```php\n$fields = [\n    'name' =\u003e ['type' =\u003e \\DB\\SQL\\Schema::DT_TEXT],\n    'mail' =\u003e ['type' =\u003e \\DB\\SQL\\Schema::DT_INT4],\n    'website' =\u003e ['type' =\u003e \\DB\\SQL\\Schema::DT_INT4],\n];\n\\DB\\Cortex::setup($db, 'users', $fields);\n``` \n\n\n### Set down\n\nThis method completely removes the specified table from the used database. So handle with care. \n\n```php\n// With Model class\n\\Model\\User::setdown();\n\n// Without Model class\n\\DB\\Cortex::setdown($db, 'users');\n``` \n\n\n## Relations\n\nWith Cortex you can create associations between multiple models. By linking them together, you can create all common relationships you need for smart and easy persistence.\n\n### Setup the linkage\n\nTo make relations work, you need to use a model class with field configuration. Cortex offers the following types of associations, that mostly **must be defined in both classes** of a relation:\n\n\u003ctable\u003e\n    \u003ctr\u003e\n        \u003cth\u003eType\u003c/th\u003e\n        \u003cth\u003eModel A\u003c/th\u003e\n        \u003cth\u003eDirection\u003c/th\u003e\n        \u003cth\u003eModel B\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e1:1\u003c/td\u003e\n        \u003ctd\u003ebelongs-to-one\u003c/td\u003e\n        \u003ctd\u003e\u0026lt;- -\u0026gt;\u003c/td\u003e\n        \u003ctd\u003ehas-one\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e1:m\u003c/td\u003e\n        \u003ctd\u003ebelongs-to-one\u003c/td\u003e\n        \u003ctd\u003e\u0026lt;- -\u0026gt;\u003c/td\u003e\n        \u003ctd\u003ehas-many\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003em:m\u003c/td\u003e\n        \u003ctd\u003ehas-many\u003c/td\u003e\n        \u003ctd\u003e\u0026lt;- -\u0026gt;\u003c/td\u003e\n        \u003ctd\u003ehas-many\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003em:m\u003c/td\u003e\n        \u003ctd\u003ebelongs-to-many\u003c/td\u003e\n        \u003ctd\u003e ---\u0026gt;\u003c/td\u003e\n        \u003ctd\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/table\u003e\n\nThis is how a field config looks with a relation:\n\n![Cortex Rel 1](https://ikkez.de/linked/cortex-class-conf.png)\n\nThis creates an aggregation between Author and News, so\n\n\u003e One News belongs to one Author.\n\n\u003e One Author has written many News.\n\n![UML 1](https://ikkez.de/linked/cortex-dia-1.png)\n\nAs a side note: `belongs-to-*` definitions will create a new column in that table, that is used to save the id of the counterpart model (foreign key field).\nWhereas `has-*` definitions are just virtual fields which are going to query the linked models by their own id (the inverse way). This leads us to the following configuration schema:\n\nFor **belongs-to-one** and **belongs-to-many**\n\n```php\n'realTableField' =\u003e [\n    'relationType' =\u003e '\\Namespace\\ClassName',\n],\n```\n\nDefining a foreign key for `belongs-to-*` is optional. The default way is to use the identifier field. For SQL engines this is either the default primary key `id` or the custom primary key that can be set with the `$primary` class property. NoSQL engines will use `_id`. If you need to define another non-primary field to join with, use `['\\Namespace\\ClassName','cKey']`.\n\nFor **has-one** and **has-many**\n\n```php\n'virtualField' =\u003e [\n    'relationType' =\u003e ['\\Namespace\\ClassName','foreignKey'],\n],\n```\n\nThe foreign key is the field name you used in the counterpart model to define the `belongs-to-one` connection.\n\n#### many-to-many\n\nThere is one special case for many-to-many relations: here you use a `has-many` type on both models, which implies that there must be a 3rd pivot table that will be used for keeping the foreign keys that binds everything together. Usually Cortex will auto-create that table upon [setup](#set-up) method, using an auto-generated table name. If you like to use a custom name for that joining-table, add a 3rd parameter to the config array of *both* models, i.e.:\n\n```php\n'tags' =\u003e [\n    'has-many' =\u003e [\\Model\\Tag::class,'news','news_tags'],\n],\n```\n\nBy default the primary key is used as reference for the record in the pivot table. In case you need to use a different field for the primary key, so can set a custom `localKey`.\n\n```php\n'tag_key' =\u003e [\n    'type' =\u003e \\DB\\SQL\\Schema::DT_VARCHAR128,\n],\n'tags' =\u003e [\n    'has-many' =\u003e [\\Model\\Tag::class,'news','news_tags',\n        'localKey' =\u003e 'tag_key'\n    ],\n],\n```\n\nFor a custom relation key (foreign key) use `relPK`:\n```php\n'tags' =\u003e [\n    'has-many' =\u003e [\\Model\\Tag::class,'news','news_tags',\n        'relPK'=\u003e 'news_id'\n    ],\n],\n```\n\n\n##### Custom pivot column names\n\nIf you're working with an existing database table, or want to use own field names for the column in the pivot table, you can set those up with the `relField` option:\n\nI.e. in the news model:\n\n```php\n'tags' =\u003e [\n    'has-many' =\u003e [\\Model\\Tag::class,'news','news_tags',\n        'relField' =\u003e 'news_id'\n    ],\n],\n```\n\nand in the tag model:\n\n```php\n'news' =\u003e [\n    'has-many' =\u003e [\\Model\\News::class,'tags','news_tags',\n        'relField' =\u003e 'tag_id'\n    ],\n],\n```\n\nThat means that the 3rd pivot table constains `news_id` and `tag_id` fields.\n\n### Working with Relations\n\nOkay finally we come to the cool part. When configuration is done and setup executed, you're ready to go.\n\n#### one-to-one\n\nTo create a new relation:\n\n```php\n// load a specific author\n$author = new \\AuthorModel();\n$author-\u003eload(['_id = ?', 2]);\n\n// create a new profile\n$profile = new ProfileModel();\n$profile-\u003estatus_message = 'Hello World';\n\n// link author and profile together, just set the foreign model to the desired property \n$profile-\u003eauthor = $author;\n \n// OR you can also just put in the id instead of the whole object here \n// (means you don't need to load the author model upfront at all)\n$profile-\u003eauthor = 23;\n \n$profile-\u003esave();\n```\n\nYou can of course do it the other way around, starting from the author model:\n\n```php\n// create a new profile\n$profile = new ProfileModel();\n$profile-\u003estatus_message = 'Hello World';\n$profile-\u003esave();\n\n// load a specific author and add that profile\n$author = new \\AuthorModel();\n$author-\u003eload(['_id = ?', 2]);\n$author-\u003eprofile = $profile;\n$author-\u003esave();\n```\n\nand to load it again:\n\n```php\n$author-\u003eload(['_id = ?', 23]);\necho $author-\u003eprofile-\u003estatus_message; // Hello World\n\n$profile-\u003eload(['_id = ?', 1]);\necho $profile-\u003eauthor-\u003ename; // Johnny English\n```\n\n#### one-to-many, many-to-one\n\nSave an author to a news record.\n\n```php\n$author-\u003eload(['name = ?','Johnny English']);\n$news-\u003eload(['_id = ?',42]);\n$news-\u003eauthor = $author; // set the object or the raw id\n$news-\u003esave();\n```\n\nnow you can get:\n\n```php\necho $news-\u003eauthor-\u003ename; // 'Johnny English'\n```\n\nThe field `author` now holds the whole mapper object of the AuthorModel. So you can also update, delete or cast it.\n\nThe getting all news by an author in the counterpart looks like this:\n\n```php\n$author-\u003eload(['_id = ?', 42]);\n$author-\u003enews; // is now an array of NewsModel objects\n\n// if you like to cast them all you can use\n$allNewsByAuthorX = $author-\u003ecastField('news'); // is now a multi-dimensional array\n```\n\n#### many-to-many, bidirectional\n\nWhen both models of a relation has a `has-many` configuration on their linkage fields, Cortex create a new reference table in setup, where the foreign keys of both models are linked together. This way you can query model A for related models of B and vice versa.\n\nTo save many collections to a model you've got several ways:\n\n```php\n$news-\u003eload(['_id = ?',1]);\n\n// array of IDs from TagModel\n$news-\u003etags = [12, 5];\n// OR a split-able string\n$news-\u003etags = '12;5;3;9'; // delimiter: [,;|]\n// OR an array of single mapper objects\n$news-\u003etags = [$tag,$tag2,$tag3];\n// OR a hydrated mapper that may contain multiple results\n$tag-\u003eload(['_id != ?',42]);\n$news-\u003etags = $tag;\n\n// you can also add a single tag to your existing tags\n$tag-\u003eload(['_id = ?',23]);\n$news-\u003etags[] = $tag;\n\n$news-\u003esave();\n```\n \nNow you can get all tags of a news entry:\n \n```php\n$news-\u003eload(['_id = ?',1]);\necho $news-\u003etags[0]['title']; // Web Design\necho $news-\u003etags[1]['title']; // Responsive\n```\n\nAnd all news that are tagged with *Responsive*:\n\n```php\n$tags-\u003eload(['title = ?','Responsive']);\necho $tags-\u003enews[0]-\u003etitle; // '10 Responsive Images Plugins'\n```\n\nThis example shows the inverse way of querying (using the TagModel to find the corresponding news). Of course the can also use a more direct way that offers even more possibilities, therefore check the [has()](#has) method.\n\n#### many-to-many, unidirectional\n\nYou can use a `belongs-to-many` field config to define a one-way m:m relation.\nThis is a special type for many-to-many as it will not use a 3rd table for reference and just puts a list of IDs into the table field, as commonly practiced in NoSQL solutions.\nThis is an unidirectional binding, because the counterpart wont know anything about its relation and it's harder to query the reserve way, but it's still a lightweight and useful solution in some cases.\n\nSaving works the same way like the other m:m type described above\n\n```php\n$news-\u003etags = [4,7]; // IDs of TagModel\n$news-\u003esave();\n```\n\nand get them back:\n\n```php\n$news-\u003eload(['_id = ?', 77]);\necho $news-\u003etags[0]-\u003etitle; // Web Design\necho $news-\u003etags[1]-\u003etitle; // Responsive\n```\n\n#### many-to-many, self-referencing\n\nIn case you want to bind a many-to-many relation to itself, meaning that you'd like to add it to the own property of the same model, you can do this too, since these are detected as self-referenced fields now.\n\n\u003ctable\u003e\n    \u003ctr\u003e\n        \u003cth\u003eType\u003c/th\u003e\n        \u003cth\u003eModel A\u003c/th\u003e\n        \u003cth\u003eDirection\u003c/th\u003e\n        \u003cth\u003eModel A\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003em:m\u003c/td\u003e\n        \u003ctd\u003ehas-many\u003c/td\u003e\n        \u003ctd\u003e\u0026lt;--\u0026gt;\u003c/td\u003e\n        \u003ctd\u003ehas-many\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/table\u003e\n\nA common scenario is where a `User` has friends and that relation target is also `User`. So it would bind the relation to itself: \n\n```php\nnamespace Model;\nclass User extends \\DB\\Cortex {\n  protected $fieldConf = [\n    'friends' =\u003e [\n      'has-many' =\u003e [\\Model\\User::class,'friends']\n    ]\n  ];\n}\n```\n\nTo use a different field name in the pivot table for the reference field, use `selfRefField` option:\n\n```php\n'friends' =\u003e [\n  'has-many' =\u003e [\\Model\\User::class,'friends',\n    'selfRefField' =\u003e 'friends_ref'\n  ]\n]\n```\n\nBecause this is also a many to many relation, a pivot table is needed too. Its name is generated based on the table and fields name, but can also be defined as 3rd array parameter, i.e. `['\\Model\\User','friends','user_friends']`.\n\n![Cortex m:m self-ref](http://ikkez.de/linked/cortex-self-ref.png)\n\nUsually, this is a bidirectional relation, meaning that you would get a direct linkage to your friends (`friends`), and another one to the inverse linkage (friends with me, `friends_ref`). As this is pretty inconvenient for further working and filtering on those, both\nfields are linked together internally and will always represent **all** relations, whether the relation was added from UserA or UserB.\n  \n```php\n$userA = new \\Model\\User();\n$userA-\u003eload(['_id = ?', 1]);\n\n$userB = new \\Model\\User();\n$userB-\u003eload(['_id = ?', 2]);\n\n$userC = new \\Model\\User();\n$userC-\u003eload(['_id = ?', 3]);\n\nif ($userA-\u003efriends)\n\t$userA[] = $userB;\nelse \n\t$userA = [$userB];\n\t\n$userA-\u003esave();\n\n$userC-\u003efriends = [$userA,$userB];\n$userC-\u003esave();\n```\n  \nThe only exception is, that the current record itself is always excluded, so you wont get UserA as friend of UserA:\n\n```php\n$userA-\u003eload(['_id = ?', 1]);\n$userA-\u003efriends-\u003egetAll('_id'); // [2,3]\n$userB-\u003eload(['_id = ?', 2]);\n$userB-\u003efriends-\u003egetAll('_id'); // [1,3]\n$userC-\u003eload(['_id = ?', 3]);\n$userC-\u003efriends-\u003egetAll('_id'); // [1,2]\n```\n  \n\n## Event Handlers\n\nCortex inherits all setters form the [Cursor Event Handlers](http://fatfreeframework.com/cursor#event-handlers) and additionally adds custom field handlers (setter/getter). These can be used to execute some extra code right before or after doing something. This could be useful for validation directly in your Model, or some extended save, load or delete cascades.\n\nThe following events are supported:\n\n* `onload`\n* `onset`\n* `onget`\n* `beforeerase`\n* `aftererase`\n* `beforeinsert`\n* `afterinsert`\n* `beforeupdate`\n* `afterupdate`\n\n\nYou can setup own handlers to this events like this:\n\n```php\n$mapper-\u003eonload(function($self){\n\t// custom code\n});\n// or \n$mapper-\u003eonload('App/Foo/Bar::doSomething');\n```\n\nYou can provide anything that is accepted by the [Base-\u003ecall](http://fatfreeframework.com/base#call) method as handler function. Notice to use the `$self-\u003eset('field','val')` instead of `$self-\u003efield=val`, if you define a handler within a child class of Cortex (i.e. an extended `__construct` in your own model class).\n\nIf any `before*` event returns a `false` result, the action that is going to be performed will be aborted, and the `after*` events are skipped. \n\n### Custom Field Handler\n\nThe `onset` and `onget` events have slightly different parameters:\n \n```php\n$mapper-\u003eonset('field',function($self, $val){\n\treturn md5($val);\n});\n```\n\nYou can also define these custom field preprocessors as a method within the class, named `set_*` or `get_*`, where `*` is the name of your field. In example:\n\n```php\nclass User extends \\DB\\Cortex {\n    // [...]\n        \n    // validate email address\n    public function set_mail($value) {\n        if (\\Audit::instance()-\u003eemail($value) == false) {\n            // no valid email address\n            // throw exception or set an error var and display a flash message\n            $value = null;\n        }\n        return $value;\n    }    \n    // hash a password before saving\n    public function set_password($value) {\n        return \\Bcrypt::instance()-\u003ehash($value);\n    } \n    public function get_name($value) {\n        return ucfirst($value);\n    }  \n}\n```\n\nSo setting these fields in your Model, like:\n\n```php\n$user-\u003epassword = 'secret';\n$user-\u003email = 'foo@bar.com';\n```\n\nwill now trigger your custom setters, doing anything you like.\n\n\n\n## Filter Query Syntax\n\nWell basically the `$filter` syntax for writing cortex queries is simple SQL. But there are some slightly modifications you should have read in these additional notes.\n\n### Operators\n\nThese common filter operators are supported: \n- relational operators: `\u003c`, `\u003e`, `\u003c=`, `\u003e=`, `==`, `=`, `!=`, `\u003c\u003e`\n- search operators: `LIKE`,`NOT LIKE`, `IN`, `NOT IN` (not case-sensitive)\n- logical operators: `(`, `)`, `AND`, `OR`, `\u0026\u0026`, (`||` only mysql and jig)\n\n**Comparison**\n\nWith comparison operators, you can do the following things:\n \n*  compare fields against other fields:\n\n\t`['foo \u003c bar']`\n\t\n*  compare fields against values:\n\n\t`['foo \u003e= 1']` or `['foo == \\'bar\\'']`\n\t\nEspecially for value comparison, it's **highly recommended** to use placeholders in your filter and bind their values accordingly. This ensures that the data mapper uses parameterized queries for better security. Placeholders go like this:\n\n*  positional bind-parameters:\n\n`['foo = ?', 1]` or `['foo = ? AND bar \u003c ?', 'baz', 7]`\n\n*  named bind-parameters:\n\n\t`['foo = :foo',':foo'=\u003e1]`\n\n\t`['foo = :foo AND bar \u003c :bar',':foo'=\u003e'hallo', ':bar'=\u003e7]`\n\t\n**Sugar**\n\n*  what's a special sugar in Cortex is, that you can also mix both types together:\n\n\t`['foo = ? AND bar \u003c :bar', 'bar', ':bar'=\u003e7]`\n\n*  and you can also reuse named parameter (not possible in raw PDO):\n\n\t`['min \u003e :num AND max \u003c :num', ':num' =\u003e 7]`\n\n*  comparison with `NULL` (nullable fields) works this easy:\n\n\t`['foo = ?', NULL]` or `['foo != ?', NULL]`\n\n**Search**\n\n*  The `LIKE` operator works the same way like the [F3 SQL search syntax](http://www.fatfreeframework.com/sql-mapper#search). The search wildcard (`%`) belongs into the bind value, not the query string.\n\n\t`['title LIKE ?', '%castle%']` or `['email NOT LIKE ?', '%gmail.com']`\n\n*  The `IN` operator usually needs multiple placeholders in raw PDO (like `foo IN (?,?,?)`). In Cortex queries you simply use an array for this, the QueryParser does the rest.\n\n\t`['foo IN ?', [1,2,3]]`\n\t\n\tYou can also use a CortexCollection as bind parameter. In that case, the primary keys are automatically used for matching:\n\t\n\t```php\n\t$fruits = $fruitModel-\u003efind(['taste = ?','sweet']);\n\t$result = $userModel-\u003efind(['favorite_fruit IN ?',$fruits])\n\t```\n\n\n### Options\n\nThe `$options` array for load operations respects the following keys:\n\n- order\n- limit\n- offset\n\nUse `DESC` and `ASC` flags for sorting fields, just like in SQL. Additional `group` settings are currently just bypassed to the underlying mapper and should work dependant on the selected db engine. Any unification on that might be handled in a future version.\n\n#### Relational sorting\n\n\u003e NB: This is currently experimental as of v1.7\n\nFor 1-n relations, you can apply a sorting rule, based on a field of a relation to your order option. You need to prefix the field you used in your `$fieldConf` for the relation with an `@` in your order definition:\n\nGiven the following field configuration:\n\n```php\n// Contracts fieldConf:\n'user' =\u003e ['belongs-to-one' =\u003e UserModel::class]\n\n// User fieldConf:\n'contracts' =\u003e ['has-many' =\u003e [ContractsModel::class,'user']]\n```\n\nThis example will paginate through all contracts records that are sorted by the relational user name: \n\n```php\n$contracts = new Contracts();\n$res = $contracts-\u003epaginate(0,10,null, ['order'=\u003e'@user.name ASC']);\n```\n\n\n## Advanced Filter Techniques\n\nWhen your application reaches the point where all basic CRUD operations are working, you probably need some more control about finding your records based on conditions for relations.\nHere comes the `has()` and `filter()` methods into play:\n\n### has\n\nThe has method adds some conditions to a related field, that must be fulfilled in addition, when the **next** find() or load() method of its parent is fired. So this is meant for limiting the main results.\n\nIn other words: Let's find all news records that are tagged by \"Responsive\".\n\n```php\n$news-\u003ehas('tags', ['title = ?','Responsive']);\n$results = $news-\u003efind();\necho $results[0]-\u003etitle; // '10 Responsive Images Plugins'\n```\n\nOf course you can also use the inverse way of querying, using the TagModel, load them by title and access the shared `$tags-\u003enews` property to find your records.\nThe advantage of the \"has\" method is that you can also add a condition to the parent as well. This way you could edit the load line into something like this:\n`$news-\u003efind(['published = ?', 1]);`. Now you can limit your results based on two different models - you only load *published* news which were tagged \"Responsive\".\n\nYou can also add multiple has-conditions to different relations:\n\n```php\n$news-\u003ehas('tags', ['title = ?','Responsive']);\n$news-\u003ehas('author', ['username = ?','ikkez']);\n$results = $news-\u003efind(['published = ?', 1], ['limit'=\u003e3, 'order'=\u003e'date DESC']);\n```\n\nNow you only load the last 3 published news written by me, which were tagged \"Responsive\", sorted by release date. ;)\n\nIf you like, you can also call them in a fluent style: `$news-\u003ehas(...)-\u003eload(...);`.\n\n### orHas\n\nSimilar to has method, but adds the has condition with an OR operator.\n\n### filter\n\nThe filter method is meant for limiting the results of relations. In example: load author x and only his news from 2014.\n\n```php\n$author-\u003efilter('news', ['date \u003e ?','2014-01-01']);\n$author-\u003eload(['username = ?', 'ikkez']);\n```\n\nThe same way like the `has()` method does, you can add multiple filter conditions. You can mix filter and has conditions too.\nOnce a `load` or `find` function is executed, the filter (and has) conditions are cleared for the next upcoming query.\n\nFilter conditions are currently not inherited. That means if you recursively access the fields of a relation ($author-\u003enews[0]-\u003eauthor-\u003enews) they get not filtered, but fully lazy loaded again.\n\n### Propagation\n\nIt is also possible to filter deep nested relations using the `.` dot style syntax. The following example finds all authors and only loads its news that are tagged with \"Responsive\":\n\n```php\n$author-\u003efilter('news.tags', ['title = ?','Responsive']);\n$author-\u003efind();\n```\n\nThe same applies for the has filter. The next example is similar to the previous one, but this time, instead of finding all authors, it only returns authors that have written a news entry that was tagged with \"Responsive\":\n\n```php\n$author-\u003ehas('news.tags', ['title = ?','Responsive']);\n$author-\u003efind();\n```\n\n**Notice:** These nested filter techniques are still experimental, so please handle with care and test your application well.\n\n\n## Insight into aggregation\n\nCortex comes with some handy shortcuts that could be used for essential field aggregation.\n\n### Counting relations\n\nSometimes you need to know how many relations a record has - i.e. for some stats or sorting for top 10 list views.\n\nTherefore have a look at the *[countRel](#countrel)* method, which let you setup a new adhoc field to the resulting records that counts the related records on `has-many` fields.\n\n```php\n// find all tags with the sum of all news that uses the tag, ordered by the top occurring tags first.\n$tag = new \\Model\\Tag();\n$tag-\u003ecountRel('news');\n$result = $tag-\u003efind(null,['order'=\u003e'count_news DESC, title']);\n```\n\nThe new field is named like `count_{$key}`, but you can also set a custom alias. As you can see, you can also use that field for additional sorting of your results. You can also combine this with the `has()` and `filter()` methods and set relation counters to nested relations with the `.` separator.\nNotice that `countRel()` only applies to the next called `find()` operation. Currently, you cannot use those virtual count field in a `$filter` query.\n\n### Virtual fields\n\nCortex has some abilities for own custom virtual fields. These might be useful to add additional fields that may contain data that is not stored in the real db table or computes its value out of other fields or functions, similar to the [custom field setters and getters](#custom-field-handler).\n\n```php\n// just set a simple value\n$user-\u003evirtual('is_online', TRUE);\n// or use a callback function\n$user-\u003evirtual('full_name', function($this) {\n\treturn $this-\u003ename.' '.$this-\u003esurname;\n});\n```\n\nYou can also use this to count or sum fields together and even reorder your collection on this fields using `$collection-\u003eorderBy('foo DESC, bar ASC')`. Keep in mind that these virtual fields only applies to your final received collection - you cannot use these fields in your filter query or sort condition before the actual find. \n\nBut if you use a SQL engine, you can use the underlying mapper abilities of virtual adhoc fields - just set this before any load or find operation is made:\n \n```php\n$mapper-\u003enewField = 'SQL EXPRESSION';\n```\n\n## Mapper API\n\n### $db\n**DB object**\n \nCan be an object of [\\DB\\SQL](http://fatfreeframework.com/sql), [\\DB\\Jig](http://fatfreeframework.com/jig) or [\\DB\\Mongo](http://fatfreeframework.com/sql),\n*OR* a string containing a HIVE key where the actual database object is stored at.\n\n### $table\n**table to work with**, string\n\nIf the table is not set, Cortex will use the `strtolower(get_class($this))` as table name.\n\n### $fluid\n**trigger SQL fluid schema mode**, boolean = false\n\n### $fieldConf\n**field configuration**, array\n\nThe array scheme is:\n\n```php\nprotected $fieldConf = [\n    'fieldName' =\u003e [\n        'type' =\u003e string\n        'nullable' =\u003e bool\n        'default' =\u003e mixed\n        'index' =\u003e bool\n        'unique' =\u003e bool\n    ]\n]\n```\n\nGet the whole list of possible types from the [Data Types Table](https://github.com/ikkez/f3-schema-builder/tree/master#column-class).\n\n*NB:* You can also add `'passThrough' =\u003e true` in order to use the raw value in *type* as data type in case you need a custom type which is not available in the data types table. \n \n### $ttl\n**default mapper schema ttl**, int = 60\n\nThis only affects the schema caching of the SQL mapper.\n\n### $rel_ttl\n**default mapper rel ttl**, int = 0\n\nThis setting in your model will add a caching to all relational queries\n\n### $primary\n**SQL table primary key**, string\n\nDefines the used primary key of the table. Default is `id` for SQL engine, and *always* `_id` for JIG and Mongo engines. \n The setup method respects this value for creating new SQL tables in your database and has to be an integer column.\n\n### load\n**Retrieve first object that satisfies criteria**\n\n```php\nbool load([ array $filter = NULL [, array $options = NULL [, int $ttl = 0 ]]])\n```\n\nSimple sample to load a user:\n\n```php\n$user-\u003eload(['username = ?','jacky']);\nif (!$user-\u003edry()) {\n    // user was found and loaded\n    echo $user-\u003eusername;\n} else {\n    // user was not found\n}\n```\n\nWhen called without any parameter, it loads the first record from the database.\nThe method returns `TRUE` if the load action was successful.\n\n### loaded\n**Count records that are currently loaded**\n\n```php\nint loaded()\n```\n\nSample:\n\n```php\n$user-\u003eload(['last_name = ?','Johnson']);\necho $user-\u003eloaded(); // 3\n```\n\n### first, last, next, prev, skip\n**Methods to navigate the cursor position and map a record**\n\nSee [http://fatfreeframework.com/cursor#CursorMethods](http://fatfreeframework.com/cursor#CursorMethods).\n\n```php\n$user-\u003eload(['last_name = ?','Johnson']);\necho $user-\u003eloaded(); // 3\necho $user-\u003e_id; // 1\n$user-\u003elast();\necho $user-\u003e_id; // 3\necho $user-\u003eprev()-\u003e_id; // 2\necho $user-\u003efirst()-\u003e_id; // 1\necho $user-\u003eskip(2)-\u003e_id; // 3\n```\n\n\n### cast\n**Return fields of mapper object as an associative array**\n\n```php\narray cast ([ Cortex $obj = NULL [, int $rel_depths = 1]])\n```\n\n#### Field masks\n\n\u003e NB: Since configuring *relations depths* seems more and more less practical, a new way of casting relations was introducted: \"Field masks\". This is the way to go and will replace the legacy \"relations depths configuration\" in a future release.\n\n \nYou can also use ``$rel_depths`` for defining a mask to mappers, so you can restrict the fields returned from a cast:\n\n```php\n$data = $item-\u003ecast(null,[\n    '_id',\n    'order.number',\n    'product._id',\n    'product.title',\n    'product.features._id',\n    'product.features.title',\n    'product.features.icon',\n]);\n```\n\n#### relation depths (old way)\n\nA simple cast sample. If the model contains relations, they are also casted for 1 level depth by default:\n\n```php\n$user-\u003eload(['_id = ?',3]);\nvar_dump($user-\u003ecast());\n/* Array (\n    [_id] =\u003e 3\n    [first_name] =\u003e Steve\n    [last_name] =\u003e Johnson\n    [comments] =\u003e Array(\n        [1] =\u003e Array (\n            [_id] = 23\n            [post] =\u003e 2\n            [message] =\u003e Foo Bar\n        ),\n        [2] =\u003e Array (\n            [_id] = 28\n            [post] =\u003e 3\n            [message] =\u003e Lorem Ipsun\n        )\n    )\n)*/\n```\n\nIf you increase the `$rel_depths` value, you can also resolve further relations down the road:\n \n```php\nvar_dump($user-\u003ecast(NULL, 2));\n/* Array (\n    ...\n    [comments] =\u003e Array(\n        [1] =\u003e Array (\n            [_id] = 23\n            [post] =\u003e Array(\n                [_id] =\u003e 2\n                [title] =\u003e Kittenz\n                [text] =\u003e ... \n            )\n            [message] =\u003e Foo Bar\n        ),\n        ...\n    )\n)*/\n```\n\n#### relation depths configuration\n\nIf you only want particular relation fields to be resolved, you can set an array to the ``$rel_depths`` parameter, with the following schema:\n\n```php\n$user-\u003ecast(NULL, [\n  '*' =\u003e 0,     // cast all own relations to the given depth, \n                // 0 doesn't cast any relation (default if this key is missing)\n  'modelA' =\u003e 0,// if a relation key is defined here, modelA is being loaded and casted,\n                // but not its own relations, because the depth is 0 for it\n  'modelB' =\u003e 1,// modelB and all its 1st level relations are loaded and casted\n  'modelC' =\u003e [...] // you can recursively extend this cast array scheme\n]);\n\n// simple sample: only cast yourself and the author model without its childs \n$news-\u003ecast(NULL,[\n    '*'=\u003e0,\n    'author'=\u003e0\n]);\n\n// nested sample: only cast yourself, \n// your own author relation with its profile and all profile relations \n$news-\u003ecast(NULL,[\n    '*'=\u003e0,\n    'author'=\u003e[\n        '*'=\u003e0,\n        'profile'=\u003e1\n    ]\n]);\n```\n\nIf you don't want any relation to be resolved and casted, just set `$rel_depths` to `0`.\nAny one-to-many relation field then just contains the `_id` (or any other custom field binding from [$fieldConf](#fieldConf)) of the foreign record,\nmany-to-one and many-to-many fields are just empty. \n\n\n### castField\n**Cast a related collection of mappers**\n\n```php\narray|null castField( string $key [, int $rel_depths = 0 ])\n```\n\n\n### find\n**Return a collection of objects matching criteria**\n\n```php\nCortexCollection|false find([ array $filter = NULL [, array $options = NULL [, int $ttl = 0 ]]])\n```\n\nThe resulting CortexCollection implements the ArrayIterator and can be treated like a usual array. All filters and counters which were set before are used once `find` is called:\n\n```php\n// find published #web-design news, sorted by approved user comments\n$news-\u003ehas('tags',['slug = ?','web-design']);\n$news-\u003efilter('comments', ['approved = ?',1]);\n$news-\u003ecountRel('comments');\n$records = $news-\u003efind(\n\t['publish_date \u003c= ? and published = ?', date('Y-m-d'), true],\n\t['order' =\u003e 'count_comments desc']\n);\n```\n\n### findByRawSQL\n**Use a raw SQL query to find results and factory them into models**\n\n```php\nCortexCollection findByRawSQL( string|array $query [, array $args = NULL [, int $ttl = 0 ]])\n```\n\nIn case you want to write your own SQL query and factory the results into the appropriate model, you can use this method. I.e.:\n\n```php\n$news_records = $news-\u003efindByRawSQL('SELECT * from news where foo \u003c= ? and active = ?',[42, 1]);\n```\n\n\n### findone\n**Return first record (mapper object) that matches criteria**\n\n```php\nCortex|false findone([ array $filter = NULL [, array $options = NULL [, int $ttl = 0 ]]])\n```\n\nThis method is inherited from the [Cursor](http://fatfreeframework.com/cursor) class. \n\n### afind\n**Return an array of result arrays matching criteria**\n\n```php\narray|null find([ array $filter = NULL [, array $options = NULL [, int $ttl = 0 [, int|array $rel_depths = 1 ]]]])\n```\n\nFinds a whole collection, matching the criteria and casts all mappers into an array, based on the `$rel_depths` configuration. \n\n\n### addToCollection\n**Give this model a reference to the collection it is part of**\n\n```php\nnull addToCollection( CortexCollection $cx )\n```\n\n\n### onload, aftererase, afterinsert, aftersave, afterupdate, beforeerase, beforeinsert, beforesave, beforeupdate\n**Define an event trigger**\n\n```php\ncallback onload( callback $func )\n```\n\nSee the guide about [Event Handlers](#event-handlers) for more details.\n\n### onget, onset\n**Define a custom field getter/setter**\n\n```php\ncallback onget( string $field, callback $func )\n```\n\nSee the guide about [Custom Field Handler](#custom-field-handler) for more details.\n\n### clear\n**Clear any mapper field or relation**\n\n```php\nnull clear( string $key )\n```\n\n\n### cleared\n**Returns whether the field was cleared or not**\n\n```php\nmixed initial( string $key )\n```\n\nIf the field initially had data, but the data was cleared from the field, it returns that old cleared data. If no initial data was present or the field has not changed (cleared) `FALSE` is returned.\n\n### clearFilter\n**Removes one or all relation filter**\n\n```php\nnull clearFilter([ string $key = null ])\n```\n\nRemoves only the given `$key` filter or all, if none was given.\n\n\n### compare\n**Compare new data against existing initial values of certain fields**\n\n```php\nnull compare( array $fields, callback $new [, callback $old = null ])\n```\n\nThis method compares new data in form of an assoc array of [field =\u003e value] against the initial field values and \ncalls a callback functions for *$new* and *$old* values, which can be used to prepare new / cleanup old data.\n\nUpdated fields are set, the *$new* callback MUST return a value.\n\n```php\n$uploads=[\n    'profile_image' =\u003e 'temp_uploads/thats_me.jpg',\n    'pictures' =\u003e ['7bbn4ksw8m5', 'temp_uploads/random_pic.jpg']\n];\n$this-\u003emodel-\u003ecompare($uploads,function($filepath) {\n    // new files\n    return $this-\u003ehandleFileUpload($filepath);\n}, function($fileId){\n    // old files\n    $this-\u003edeleteFile($fileId);\n});\n```\n\nIn the example above, we handle multiple fields and compare their values with an incoming array for new data. For each new field value or changed / added array item value, the `$new` function is called. For existing data, that's not present in the new data anymore, the `$old` function is called. \n\n### copyfrom\n**Hydrate the mapper from hive key or given array**\n\n```php\nnull copyfrom( string|array $key [, callback|array|string $fields = null ])\n```\n\nUse this method to set multiple values to the mapper at once. \nThe `$key` parameter must be an array or a string of a hive key, where the actual array can be found.\n\nThe `$fields` parameter can be a splittable string:\n \n```php\n$news-\u003ecopyfrom('POST','title;text');\n```\n\nOr an array:\n\n```php\n$news-\u003ecopyfrom('POST',['title','text']);\n```\n\nOr a callback function, which is used to filter the input array:\n\n\n```php\n$news-\u003ecopyfrom('POST',function($fields) {\n    return array_intersect_key($fields,array_flip(['title','text']));\n});\n```\n\n### copyto\n**Copy mapper values into hive key**\n\n```php\nnull copyto( string $key [, array|string $relDepth = 0 ])\n```\n\n### copyto_flat\n**Copy mapper values to hive key with relations being simple arrays of keys**\n\n```php\nnull copyto_flat( string $key )\n```\n\nAll `has-many` relations are being returned as simple array lists of their primary keys.\n\n\n### count\n**Count records that match criteria**\n\n```php\nnull count([ array $filter [, array $options = NULL [, int $ttl = 60 ]]])\n```\n\nJust like `find()` but it only executes a count query instead of the real select.\n\n\n### countRel\n**add a virtual field that counts occurring relations**\n\n```php\nnull countRel( string $key [, string $alias [, array $filter [, array $option]]])\n```\n\nThe `$key` parameter must be an existing relation field name. This adds a virtual counter field to your result,\nwhich contains the count/sum of the matching relations to the current record, which is named `count_{$key}`, unless you define a custom `$alias` for it.\n\nIt's also possible to define a `$filter` and `$options` to the query that's used for counting the relations.\n\nYou can also use this counter for sorting, like in this tag-cloud sample:\n\n```php\n$tags = new \\Model\\Tag();\n$tags-\u003efilter('news',['published = ? and publish_date \u003c= ?', true, date('Y-m-d')]);\n$tags-\u003ecountRel('news');\n$result = $tags-\u003efind(['deleted = ?',0], ['order'=\u003e'count_news desc']);\n```\n\nThis method also supports propagation, so you can define counters on nested relations pretty straightforward:\n\n```php\n// fetch all posts, with comments and count its likes (reactions of type \"like\") on each comment\n$post-\u003ecountRel('comments.reaction','count_likes', ['type = ?', 'like']);\n$results = $post-\u003efind();\n```\n\n\n### dbtype\n**Returns the currently used db type**\n\n```php\nstring dbtype()\n```\n\nThe type is `SQL`, `Mongo` or `Jig`.\n\n\n### defaults\n**Return default values from schema configuration**\n\n```php\narray defaults([ bool $set = FALSE ])\n```\n\nReturns a `$key` =\u003e `$value` array of fields that has a default value different than `NULL`.\n\n### dry\n**Return TRUE if current cursor position is not mapped to any record**\n\n```php\nbool dry()\n```\n\nSample:\n\n```php\n$mapper-\u003eload(['_id = ?','234']);\nif ($mapper-\u003edry()) {\n    // not found\n} else {\n    // record was loaded\n}\n```\n\n### erase\n**Delete object/s and reset ORM**\n\n```php\nnull erase([ array $filter = null ])\n```\n\nWhen a `$filter` parameter is set, it deletes all matching records:\n\n```php\n$user-\u003eerase(['deleted = ?', 1]);\n```\n \nIt deletes the loaded record when called on a hydrated mapper without `$filter` parameter:\n\n```php\n$user-\u003eload(['_id = ?',6]);\n$user-\u003eerase();\n```\n\nThis also calls the `beforeerase` and `aftererase` events.  \n\n### exists\n**Check if a certain field exists in the mapper or is a virtual relation field**\n\n```php\nbool exists( string $key [, bool $relField = false ])\n```\n\nIf `$relField` is true, it also checks the [$fieldConf](#fieldConf) for defined relational fields.\n\n\n### fields\n**get fields or set whitelist / blacklist of fields**\n\n```php\narray fields([ array $fields = [] [, bool $exclude = false ])\n```\n\nWhen you call this method without any parameter, it returns a list of available fields from the schema.\n\n```php\nvar_dump( $user-\u003efields() );\n/* Array(\n    '_id'\n    'username'\n    'password'\n    'email'\n    'active'\n    'deleted'\n)*/\n```\n\nIf you set a `$fields` array, it'll enable the field whitelisting, and put the given fields to that whitelist. \nAll non-whitelisted fields on loaded records are not available, visible nor accessible anymore. This is useful when you don't want certain fields in a returned casted array.\n\n```php\n$user-\u003efields(['username','email']); // only those fields\n$user-\u003eload();\nvar_dump($user-\u003ecast());\n/* Array(\n    '_id' =\u003e 5\n    'username' =\u003e joe358\n    'email' =\u003e joe@domain.com\n)*/\n```\n\nCalling this method will re-initialize the mapper and takes effect on any further load or find action, so run this first of all.\n\nIf you set the `$exclude` parameter to `true`, it'll also enable the whitelisting, but set all available fields, without the given, to the whitelist. \nIn other words, the given $fields become blacklisted, the only the remaining fields stay visible. \n\n```php\n$user-\u003efields(['email'], true); // all fields, but not these\n$user-\u003eload();\nvar_dump($user-\u003ecast());\n/* Array(\n    '_id' =\u003e 5\n    'username' =\u003e joe358\n    'password' =\u003e $18m$fsk555a3f2f08ff28\n    'active' =\u003e 1\n    'deleted' =\u003e 0\n)*/\n```\n\nIn case you have relational fields configured on the model, you can also prohibit access for the fields of that relations. For that use the dot-notation:\n\n```php\n$comments-\u003efields(['user.password'], true); // exclude the password field in user model\n$comments-\u003eload();\nvar_dump($comments-\u003ecast());\n/* Array(\n    '_id' =\u003e 53\n    'message' =\u003e ....\n    'user' =\u003e Array(\n        '_id' =\u003e 5\n        'username' =\u003e joe358\n        'active' =\u003e 1\n        'deleted' =\u003e 0\n    ) \n)*/\n```\n\nYou can call this method multiple times in conjunction. It'll always merge with your previously set white and blacklisted fields.\n`_id` is always present.\n\n\n### filter\n**Add filter for loading related models**\n\n```php\nCortex filter( string $key [, array $filter = null [, array $options = null ]])\n```\n\nSee [Advanced Filter Techniques](#advanced-filter-techniques).\n\n### get\n**Retrieve contents of key**\n\n```php\nmixed get( string $key [, bool $raw = false ])\n```\n\nIf `$raw` is `true`, it'll return the raw data from a field as is. No further processing, no relation is resolved, no get-event fired.\nUseful if you only want the raw foreign key value of a relational field.\n\n### getRaw\n**Retrieve raw contents of key**\n\n```php\nmixed getRaw( string $key )\n```\n\nThis is a shortcut method to `$mapper-\u003eget('key', TRUE)`.\n\n### getFieldConfiguration\n**Returns model $fieldConf array**\n\n```php\narray|null getFieldConfiguration()\n```\n\n### getTable\n**returns model table name**\n\n```php\nstring getTable()\n```\n\nIf no table was defined, it uses the current class name to lowercase as table name.\n\n### has\n**Add has-conditional filter to next find call**\n\n```php\nCortex has( string $key [, array $filter = null [, array $options = null ]])\n```\n\nSee [Advanced Filter Techniques](#advanced-filter-techniques).\n\n### orHas\n**Add has-conditional filter with OR operator to previous condition**\n\nSame as has filter, but chains with a logical OR to the previous condition.\n\n\n### initial\n**Return initial field value**\n\n```php\nmixed initial( string $key )\n```\n\nReturns the initial data from a field, like it was fetched from the database, even if the field as changed afterwards. Array fields are decoded / unserialized properly before it's returned.\n\n### mergeFilter\n**Glue multiple filter arrays together into one**\n\n```php\narray mergeFilter( array $filters [, string $glue = 'and' ])\n```\n\nThis is useful when you want to add more conditions to your filter array or want to merge multiple filter arrays together, i.e. when you assemble the filter for a complex search functionality which is based on conditions.\nUse the `$glue` parameter to define the part that is used to merge two filters together (usually `AND` or `OR`).\n\n```php\n$filter1 = ['_id = ?', 999];\n$filter2 = ['published = ? or active = ?', true, false];\n\n$new_filter = $mapper-\u003emergeFilter([$filter1, $filter2]);\n// array('(_id = ?) and (published = ? or active = ?)', 999, true, false)\n```\n\n### paginate\n**Return array containing subset of records matching criteria**\n\n```php\narray paginate([ int $pos = 0 [, int $size = 10 [, array $filter = NULL [, array $options = NULL [, int $ttl = 0 ]]]]])\n```\n\nSee [Cursor-\u003epaginate](http://fatfreeframework.com/cursor#paginate). Any *has* and *filter* filters can be used in conjunction with paginate as well. \n\n\n### rel\n**returns a clean/dry model from a relation**\n\n```php\nCortex rel( string $key )\n```\n\nFor instance, if `comments` is a one-to-many relation to `\\Model\\Comment`:\n\n```php\n$user-\u003eload();\nvar_dump($user-\u003ecomments); // array of comments\n$new_comment = $user-\u003erel('comments'); \n// $new_comment is a new empty \\Model\\Comment\n```\n\n\n### reset\n**reset and re-initialize the mapper**\n\n```php\nnull reset([ bool $mapper = true ])\n```\n\nIf `$mapper` is *false*, it only reset filter, default values and internal caches of the mapper, but leaves the hydrates record untouched.\n\n### resetFields\n**reset only specific fields and return to their default values**\n\n```php\nnull resetFields( array $fields )\n```\n\nIf any field doesn't have a default value, it's reset to `NULL`.\n\n### resolveConfiguration\n**kick start mapper to fetch its config**\n\n```php\narray resolveConfiguration()\n```\n\nReturns an array that exposes a mapper configuration. The array includes:\n\n*\ttable\n*\tfieldConf\n*\tdb\n*\tfluid\n*\tprimary\n\n\n### save\n**Save mapped record**\n\nIt is recommended to always use the save method. It'll automatically see if you want to save a new record or update an existing, loaded record.\n\n```php\n$user-\u003eusername = 'admin'\n$user-\u003eemail = 'admin@domain.com';\n$user-\u003esave(); // insert\n\n$user-\u003ereset();\n$user-\u003eload(['username = ?','admin']);\n$user-\u003eemail = 'webmaster@domain.com';\n$user-\u003esave(); // update\n```\n\nThe save method also fires the `beforeinsert`, `beforeupdate`, `afterinsert` and `afterupdate` events. \nThere are also `insert`and `update`method, but using that methods directly, will skip the events and any cascading actions.\n\n\n### set\n**Bind value to key**\n\n```php\nmixed set( string $key, mixed $val )\n```\n\n### setdown\n**erase all model data, handle with care**\n\n```php\nnull setdown([ object|string $db = null [, string $table = null ]])\n```\n\nThis method completely drops the own table, and used many-to-many pivot-tables from the database. \n\n\n### setFieldConfiguration\n**set model definition**\n\n```php\nnull setFieldConfiguration( array $config )\n```\n\nUsed to set the **$fieldConf** array.\n\n\n### setup\n**setup / update table schema**\n\n```php\nbool setup([ object|string $db = null [, string $table = null [, array $fields = null ]]])\n```\n\nThis method creates the needed tables for the model itself and additionally required pivot tables. It uses the internal model properties *$db*, *$table* and *fieldConf*, \nbut can also be fed with method parameters which would take precedence. \n\n\n### touch\n**update a given date or time field with the current time**\n\n```php\nnull touch( string $key [, int $timestamp = NULL ])\n```\n\nIf `$key` is a defined field in the *$fieldConf* array, and is a type of date, datetime or timestamp,\nthis method updates the field to the current time/date in the appropriate format.\n\nIf a `$timestamp` is given, that value is used instead of the current time.\n\n\n### valid\n**Return whether current iterator position is valid.**\n\n```php\nbool valid()\n```\n\nIt's the counterpart to [dry()](#dry).\n\n```php\n$mapper-\u003eload(['_id = ?','234']);\nif ($mapper-\u003evalid()) {\n    // record was loaded\n} else {\n    // not found\n}\n```\n\n\n### virtual\n**virtual mapper field setter**\n\n```php\nnull virtual( string $key, mixed $val )\n```\n\nThis sets a custom virtual field to the mapper. Useful for some on-demand operations:\n\n```php\n$user-\u003evirtual('pw_unsecure', function($this) {\n    return \\Bcrypt::instance()-\u003eneeds_rehash($this-\u003epassword, 10);\n});\n```\n\nIt is possible to use the virtual fields for a post-sorting on a selected collection, see [virtual fields](#virtual-fields).\n\n\n## Collection API\n\nWhenever you use the `find` method, it will return an instance of the new CortexCollection class. This way we are able determine the whole collection from the inside of every single mapper in the results, and that gives us some more advanced features, like the [smart-loading of relations](https://github.com/ikkez/F3-Sugar/issues/23#issuecomment-24956163). The CortexCollection implements the `ArrayIterator` interface, so it is accessible like an usual array. Here are some of the most useful methods the Cortex Collection offers:\n\n### add\n**add single model to collection**\n\n```php\nnull add( Cortex $model )\n```\n\nIt's also possible to use the array notation to add models:\n\n```php\n$news-\u003eload();\n$new_comment = $news-\u003erel('comments');\n$new_comment-\u003etext = 'Foo Bar';\n$news-\u003ecomments[] = $new_comment;\n$news-\u003esave();\n```\n\n\n### castAll\n**cast all contained mappers to a nested array**\n\n```php\narray castAll([ $reldepths = 1 ])\n```\n\nSimilar to the `Cortex-\u003ecast` method for a single mapper, this automatically casts all containing mappers to a simple nested array.\n\n```php\n$result = $news-\u003efind(['published = ?',true]);\nif ($result)\n    $json = json_encode($result-\u003ecastAll());\n```\n\nUse the `$reldepths` parameter to define what to cast, see [cast](#cast) method for details.\n\n### compare\n**compare collection with a given ID stack**\n\n```php\narray compare( array|CortexCollection $stack [, string $cpm_key = '_id'])\n```\n\nThis method is useful to compare the current collection with another collection or a list of values that is checked for existence in the collection records. \n\nIn example you got a relation collection that is about to be updated and you want to know which records are going to be removed or would be new in the collection:\n\n```php\n$res = $user-\u003efriends-\u003ecompare($newFriendIds);\nif (isset($res['old'])) {\n\t// removed friends\n}\nif (isset($res['new'])) {\n\t// added friends\n\tforeach($res['new'] as $userId) {\n\t\t// do something with $userId\n\t}\n}\n```\n\nThe compare result `$res` is an array that can contain the array keys `old` and `new`, which both represent an array of `$cpm_key` values.  \n\nNB: This is just a comparison - it actually does not update any of the collections. Add a simple `$user-\u003efriends = $newFriendIds;` after comparison to update the collection.\n \n\n### contains\n**check if the collection contains a record with the given key-val set**\n\n```php\nbool contains( mixed|Cortex $val [, string $key = '_id' ])\n```\n\nThis method can come handy to check if a collections contains a given record, or has a record with a given value:\n\n```php\nif ($user-\u003efriends \u0026\u0026 $user-\u003efriends-\u003econtains($newFriend)) {\n\t$f3-\u003eerror(400,'This user is already your friend');\n\treturn;\n}\n```\n\nWith custom compare key:\n\n```php\nif ($user-\u003eblocked_users-\u003econtains($currentUserId,'target')) {\n\t// this user has blocked you\n}\n```\n\n\n### expose\n**return internal array representation**\n\n```php\narray expose()\n```\n\n### factory\n**create a new collection instance from given records**\n\n```php\nCortexCollection factory( array $records )\n```\n\n`$records` must be an array, containing Cortex mapper objects.\n\n### getAll\n**returns all values of a specified property from all models**\n\n```php\narray getAll( string $prop [, bool $raw = false ])\n```\n\nYou can fetch all values of a certain key from all containing mappers using `getAll()`. Set the 2nd argument to `true` to get only the raw DB results instead of resolved mappers on fields that are configured as a relation.\n\n```php\n$users = $user-\u003efind(['active = ?',1]);\n$mails = $users-\u003egetAll('email'); \n/* Array(\n    'user1@domain.com',\n    'user2@domain.com',\n    'user3@domain.com'\n)*/\n```\n\n\n### getBy\n\n```php\narray getBy( string $index [, bool $nested = false ])\n```\n\nYou can transpose the results by a defined key using `getBy()`.\nTherefore you need to provide an existing field in the mapper, like this;\n \n```php\n$pages = $page-\u003efind();\n$pages_by_slug = $pages-\u003egetBy('slug');\n```\n \nThis will resort the resulting array by the email field of each mapper, which gives you a result array like `array(\"foo@domain.com\"=\u003earray(...))`. If you provide `true` as 2nd argument, the records are ordered into another array depth, to keep track of multiple results per key.\n \n### hasChanged\n**returns true if any model was modified after it was added to the collection**\n\n```php\nbool hasChanged()\n```\n\n### orderBy\n**re-assort the current collection using a sql-like syntax**\n\n```php\nnull orderBy( string $cond )\n```\n\nIf you need to re-sort a result collection once more to another key, use this method like `$results-\u003eorderBy('name DESC');`. This also works with multiple sort keys.\n\n\n### setModels\n**set a collection of models**\n\n```php\narray setModels( array $models [, bool $init = true ])\n```\n\nThis adds multiple Cortex objects to the own collection. When `$init` is `true`, added models with this method wont effect the **changed** state. \n\n### slice\n**slice the collection**\n\n```php\nnull slice( int $offset [, int $limit = null ])\n```\n\nThis removes a part from the collection.\n\n\n\n## Additional notes\n\n* To release any relation, just set the field to `NULL` and save the mapper.\n\n* All relations are lazy loaded to save performance. That means they won't get loaded until you access them by the linked property or cast the whole parent model.\n\n* lazy loading within a result collection will **automatically** invoke the eager loading of that property **to the whole set**. The results are saved to an [Identity Map](https://martinfowler.com/eaaCatalog/identityMap.html) to relieve the strain on further calls. I called this _smart loading_ and is used to get around the [1+N query problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/) with no need for extra configuration.\n\n* If you need to use a primary key in SQL that is different from `id` (for any legacy reason), you can use the `$primary` class property to set it to something else. You should use the new custom pkey in your queries now. Doing so will limit your app to SQL engines.\n\n* to get the id of any record use `$user-\u003e_id;`. This even works if you have setup a custom primary key.\n\n* To find any record by its **id** use the field `_id` in your filter array, like `['_id = ?', 123]`.\n\n* primary fields should not be included in the `$fieldConf` array. They could interfere with the [setup](#set-up) routine.\n\n* There are some little behaviours of Cortex you can control by these hive keys:\n\n\t* `CORTEX.queryParserCache`: if `TRUE` all query strings are going to be cached too (may add a lot of cache entries). Default: `FALSE`\n\n\t* `CORTEX.smartLoading`: triggers the intelligent-lazy-eager-loading. Default is `TRUE`, but turn it off if you think something works wrong. Could cause a lot of extra queries send to your DB, if deactivated.\n\n\t* `CORTEX.standardiseID`: Default `TRUE`. This moves any defined primary key into the `_id` field on returned arrays. \n\n\t* `CORTEX.quoteConditions`: Default `TRUE`. By default, all field names in where conditions are quoted automatically according to the used database engine. This helps to work around reserved names in SQL. However the detection of fields isn't perfect yet, so in case you want to add the correct backticks or other quotation yourself, set this to `FALSE`.\n\n## Known Issues\n\n* Not really a bug, but returned collections (from relations, *find*, or *paginate* method) are not cloneable because they need to keep a unique references to the identity map of its relations. This leads to the point that all containing mappers are not automatically escaped in templates, regardless of the `ESCAPE` setting. Keep in mind to add the `| esc` filter to your tokens.\n\nIf you find any issues or bugs, please file a [new Issue](https://github.com/ikkez/F3-Sugar/issues) on github or write a mail. Thanks.\n\n## Roadmap\n\nIf you have any ideas, suggestions or improvements, feel free to add an issue for this on github.\n\n\nCortex currently only reflects to the most common use cases. If you need more extensive control over your queries or the DB, you may consider to use the underlying mapper or DB directly. This could be done in custom methods or field preprocessors in your Model classes.\n\nAnyways, I hope you find this useful. If you like this plugin, why not make a donation?\n\n[![buy me a Beer](https://ikkez.de/linked/Beer/bdb_small_single.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=44UHPNUCVP7QG)\n\nIf you like to see Cortex in action, have a look at [fabulog](https://github.com/ikkez/fabulog \"the new fabulous blog-ware\").\n\nLicense\n-\n\nGPLv3\n  \n\n    \n","funding_links":["https://github.com/sponsors/ikkez","https://buymeacoffee.com/ikkez","https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=44UHPNUCVP7QG"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fikkez%2Ff3-cortex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fikkez%2Ff3-cortex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fikkez%2Ff3-cortex/lists"}