granadaorm / granada
Active Record / ORM with eager loading, lazy loading and more
Requires
- php: >=8.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.0
Suggests
- granadaorm/builder: Automatically build model classes from the database schema
- granadaorm/form: Quick generate html forms from the model structure using css frameworks such as Bootstrap and Bulma
Provides
None
Conflicts
None
Replaces
- surt/granada: *
- dev-master
- 6.0.3
- 6.0.2
- 6.0.1
- 6.0.0
- 5.1.0
- 5.0.0
- 4.1.1
- 4.1.0
- 4.0.10
- 4.0.9
- 4.0.8
- 4.0.7
- 4.0.6
- 4.0.5
- 4.0.4
- 4.0.3
- 4.0.2
- 4.0.1
- 4.0.0
- 3.1.6
- 3.1.5
- 3.1.4
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.0
- 2.10.12
- 2.10.11
- 2.10.10
- 2.10.9
- 2.10.8
- 2.10.7
- 2.10.6
- 2.10.5
- 2.10.4
- 2.10.3
- 2.10.2
- 2.10.1
- 2.10.0
- 2.9.1
- 2.9.0
- 2.8.4
- 2.8.3
- 2.8.2
- 2.8.1
- 2.8.0
- 2.7.0
- 2.6.1
- 2.6.0
- 2.5.0
- 2.4.1
- 2.4.0
- 2.3.0
- 2.2.0
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- dev-joshbmarshall-patch-1
This package is auto-updated.
Last update: 2026-10-27 06:28:41 UTC
README
Granada is a easy to use Active Record implementation, and ORM based on Idiorm/Paris.
Upgrading from 6.x? Read the upgrading guide.
A quick view
use Granada\Model; class User extends Model { public function posts() { return $this->has_many('Post'); } } class Post extends Model {} // select $user = User::q()->where('name', 'John')->find_one(); // modify $user->first_name = 'Doe'; $user->save(); // select relationship $posts = $user->posts()->find_many(); foreach ($posts as $post) { echo $post->content; }
You can read the Paris Docs on paris.readthedocs.org but be sure to read the additions below.
Install
Using composer:
composer require granadaorm/granada
Configure it:
require 'vendor/autoload.php'; use Granada\ORM; ORM::configure('mysql:host=localhost;dbname=my_database'); ORM::configure('username', 'database_user'); ORM::configure('password', 'top_secret');
As always, you can check it in detail on Paris documentation
Additions
Eager loading
You can use the "with" method to add relationships eager loading to the query.
$results = User::q()->with('avatar', 'posts')->find_many();
will use 3 querys to fetch the users and the relationships:
SELECT * FROM user SELECT * FROM avatar WHERE user_id IN (....) SELECT * FROM posts WHERE user_id IN (....)
It is possible to get the relationships results for each result, this way
foreach($results as $result){ echo $result->avatar->img; foreach($result->posts as $post){ echo $post->title; } }
Lazy loading
Triying to access to a not fetched relationship will call and return it
$results = User::q()->find_many(); foreach($results as $result){ echo $result->avatar->img; }
Notice that if there is no result for avatar on the above example it will throw a Notice: Trying to get property of non-object...
Note: Maybe worth the effort to create a NULL object for this use case and others.
Lazy finds
find_many_lazy() is find_many() for big tables. Use it when the
result is too big to hold in memory, for example a report over many
thousands of rows. The loop looks the same, but your script holds
one model at a time:
foreach (User::q()->where('active', 1)->find_many_lazy() as $id => $user) { echo $user->name; }
The order and the array keys are the same as find_many() gives
you.
The chunk size
The method loads the rows in chunks. The first argument is the chunk size. The default is 1000:
// 200 rows per query foreach (User::q()->find_many_lazy(200) as $user) { ... }
A smaller chunk uses less memory and runs more queries. A bigger chunk does the opposite. The results you get are identical.
Eager loading
with() works as usual. The relationship query runs once per chunk,
not once per model:
foreach (Car::q()->with('parts')->find_many_lazy() as $car) { echo $car->parts[0]->name; }
Loop only once
The method returns a generator. A generator cannot restart. To loop
again, call find_many_lazy() again. Each call runs the queries
again, so the data may have changed.
Queries the method refuses
The method throws an exception when the query has one of these:
group_by()- the method loads each row by its id, and a grouped row has no single idraw_query()- the method loads the rows by rebuilding the query, and it cannot rebuild raw SQL- a
join()plus a customselect()- the rows are loaded from the table alone, so the select may only name that table's columns
If you turned on return_result_sets, it does not apply here: the
method always returns a generator.
For small results, keep using find_many().
Eager loading with query control
Use with_{relation}() to eager-load a relationship and optionally configure the query via a callback:
// Basic - equivalent to with('manufactor') $car = Car::q()->with_manufactor()->find_one(1); // Limit columns selected $car = Car::q()->with_manufactor(fn($q) => $q->select('id, name'))->find_one(1); // SELECT `id`, `name` FROM `manufactor` WHERE ... // Add ordering (has_many - multiple related records) $manufactor = Manufactor::q()->with_cars(fn($q) => $q->select('id, name')->order_by_desc('name'))->find_one(1); // SELECT `id`, `name`, `manufactor_id` FROM `car` WHERE ... ORDER BY `name` DESC
Mix with the existing with() method:
$car = Car::q()->with_manufactor(fn($q) => $q->select('name'))->with('owner')->find_one(1);
Nested eager loading via with_* inside the callback:
$owner = Owner::q()->with_car(fn($q) => $q->select('id, manufactor_id') ->with_manufactor(fn($q) => $q->select('name')) )->find_one(1);
Auto-included columns. You don't need to specify the columns the ORM uses for matching. They are added automatically:
| Relationship type | Column auto-added |
|---|---|
belongs_to |
The related model's primary key |
has_one, has_many |
The foreign key on the related table |
If you already include these columns in your select(), they won't be added twice.
Column names. Use comma-separated strings for multiple columns: select('id, name'). Do not use select('id', 'name') - the second argument is an alias.
has_many_through. Qualify column names with the table because the query involves a join:
$car = Car::q()->with_parts(fn($q) => $q->select('part.id, part.name'))->find_one(1);
Chained relationships with arguments for eager loading
It is possible to chain relationships and add arguments to the relationships calls
// chained relationships with dot notation $results = User::q()->with('posts.comments')->find_many(); // OR // chained relationships use the "with" reserved word. (usefull if you want to pass arguments to the relationships) $results = User::q()->with(array('posts'=>array('with'=>array('comments'))))->find_many(); // SELECT * FROM user // SELECT * FROM post WHERE user_id IN (....) // SELECT * FROM comments WHERE post_id IN (....) foreach($results as $result){ foreach($posts as $post){ echo $post->title; foreach($post->comments as $comment){ echo $comment->subject; } } } // you can use arguments (one or more) to call the models relationships $results = User::q()->with(array('posts'=>array('arg1')))->find_many(); // will call the relationship defined in the user model with the argument "arg1"
Custom query actions
We use a query class to build filters for a query.
To add custom filters to a Query model, create a Query<ModelName> class that extends \Granada\Query.
class QueryModelName extends \Granada\Query { public function aname($argument1, $argument2): static { return $this->where('property', 'value')->limit(10); } }
and call it through q():
ModelName::q()->aname($argument1, $argument2)->...
Multiple additions names for Granada
- select_raw
- group_by_raw
- order_by_raw
- raw_join
- insert : To create and save multiple elements from an array
- pluck : returns a single column from the result.
- find_pairs : Return array of key=>value as result
- delete_many : removes every row the query matches
- update_many : writes the same values to every row the query matches
Overload SET
// In the Model protected function set_title($value) { $this->alias = strtoupper($value); return $value; }
// outside of the model $content_instance->set('title', 'A title'); // works with multiple set too $properties = array( 'title' => 'A title', 'content' => 'Some content' ); $content_instance->set($properties); // try it with a direct assignement $content_instance->title = 'A title';
Mass assignment
create() and set() write every key they are given. There is no fillable or
guarded list to declare: Granada does not know what is safe to write, and the
model does not filter for you.
If the array comes from a request, keep only the fields the request was meant to set before it reaches the model:
$allowed = array('name', 'email', 'phone'); $enquiry = Enquiry::create(array_intersect_key($data, array_flip($allowed)));
Unknown keys are not dropped. They are stored like any other field, and the save fails with a database error if the column does not exist. Filtering request data is the caller's job, because the caller is the one who knows the fields the form offered.
Overload GET and MISSING property
// In the Model // Work on defined protected function get_path($value) { return strtolower($value); } // and non-defined attributes. protected function mising_testing() { return 'whatever'; } ... // outside of the model echo $content_instance->path; // returns the lowercase path value of $content_instance echo $content_instance->testing; // returns 'whatever' since we defined a missing_{attribute_name}
Of course, you still can define functions with the property name if you want to overload it completely.
Define resultSet (collection type) class on Model
Now is possible to define the resultSet class returned for a model instances result. (if return_result_sets config variable is set to true)
Notice that the resultSet class defined must extends Granada\ResultSet and must be loaded
// In the Model public static $resultSetClass = 'TreeResultSet';
// outside of the model var_dump(Content::q()->find_many()); // echoes object(TreeResultSet)[10] protected '_results' => array(...) ....
ResultSets are defined by the model in the result, as you can see above.
On eager load, the results are consistent.
For example, if we have a Content model, with $resultSetClass = 'TreeResultSet' and a has_many relationship defined as media:
Content::q()->with('media')->find_many();
will return a TreeResultSet with instances of Content each with a property $media containing Granada\ResultSet (the default resultSet if none if defined on the Model)
Basic Documentation comes from Paris
Paris
Feature complete
Paris is now considered to be feature complete as of version 1.4.0. Whilst it will continue to be maintained with bug fixes there will be no further new features added.
A lightweight Active Record implementation for PHP5.
Built on top of Idiorm.
Tested on PHP 5.2.0+ - may work on earlier versions with PDO and the correct database drivers.
Released under a BSD license.
Features
- Extremely simple configuration.
- Exposes the full power of Idiorm's fluent query API.
- Supports associations.
- Simple mechanism to encapsulate common queries in filter methods.
- Built on top of PDO.
- Uses prepared statements throughout to protect against SQL injection attacks.
- Database agnostic. Currently supports SQLite, MySQL, Firebird and PostgreSQL. May support others, please give it a try!
- Supports collections of models with method chaining to filter or apply actions to multiple results at once.
- Multiple connections are supported
History
Granada was originally developed by Erik Wiesenthal, up to version 1.5. Further development started by Josh Marshall to release version 2.0