On Github moufmouf / tdbm4-slides
Created by TheCodingMachine / @david_negrier
An ORM is a library that maps your database records to your objects.
In some libraries, everything starts with the database model. This is sometimes called "ActiveRecord ORMs". TDBM is part of this category. It's maybe the most advanced ORM when it comes to understanding your data model and translating it to actual PHP classes.
In other libraries, everything starts with the PHP classes. Doctrine ORM is (somewhat) part of these tools.
The data model is already available
The objects are not owned by the application developer. They are ultimately owned by the ORM tool. This can have impacts when migrating from one version to another.
If you are into Domain Driven Development, you will have to write your domain classes once, and map them to the classes of the ORM. This means 2 classes for the same model. Do not do that. If you have a complex model and want to do DDD, use Doctrine!
Since the objects are owned by the ORM tool, the ORM can apply strong optimizations technique to these objects.
Active records (and TDBM in particular) works great for simple to medium-sized projects where your database structure maps closely your screens structure (CRUD design)
For more complex projects, use Doctrine ORM.
Your database model is meaningful. TDBM will do its best to understand it and to generate the object model that maps your database model as closely as possible.
Alternatively, if you are not using Mouf, you can use the TDBMService::generateAllDaosAndBeans() method.
In TDBM 4.1, I plan to add a Symfony 2 console command too, and more integration with other frameworks.
$john = new PersonBean('John Doe', new DateTime()); // Write values: $john->setName('John Wayne'); // Read values: $name = $john->getName(); $personDao->save($john);
You use the constructor to build your bean. All not nullable columns are parameters in the constructor.
TDBM will generate for you getters and setters.
SQL date columns are converted to PHP DateTime objects.
SQL enum columns are not supported yet (coming in TDBM 4.1).
// Let's get the bean $person = $personDao->getById(1); // Let's display the name echo $person->getName();
Supports multi-column primary keys (pass an array of column=>values).
// Let's get the list of users $userList = $userDao->findAll(); // Let's display the names foreach ($userList as $userBean) { /* @var $userBean UserBean */ echo $userBean->getName()."\n"; }
class CountryDao extends CountryBaseDao { /** * Returns the list of countries starting with $firstLetter * * @param string $firstLetter * @return CountryBean[] */ public function getCountriesByLetter($firstLetter) { return $this->find( "label LIKE :label", [ "label" => $firstLetter.'%' ]); } }
The CountryDao class can safely be edited. Never edit the CountryBaseDao as it is regenerated by TDBM on every DB model change.
Important! The find method is protected. You should only call it from within DAOs. This way, SQL code is restricted into the DAO and does not leak into the outside world.
VERY IMPORTANT! Never ever ever concatenate user input in the SQL string. Use parameters instead.
It is a security issue (SQL injection) It prevents TDBM from caching the structure of the request and will lead to extremely poor performanceUnderstood?
// BAD $list = $this->find("name LIKE '".$firstLetter.'%"' ); // GOOD $list = $this->find("label LIKE :label", [ "label" => $firstLetter.'%' ]);
class ProductDao extends ProductBaseDao { public function getProductsByCategoryAndStore( CategoryBean $category = null, StoreBean $store = null) { return $this->find( "category_id = :category AND store_id = :store", [ "category" => $category?$category->getId():null, "store" => $store?$store->getId():null ]); } }
If the $category or $store is null, the category_id or store_id field will be completely discarded from the SQL.
If a parameter is null, the filter field is rewritten to remove the part of the query that contains the null value.
"category_id = :category AND store_id = :store", [ "category" => 42, "store" => null ]
will result in the SQL filter
category_id = 42
See how the store_id has disappeared?
If you are confident that your query will only ever return one record, then, you can use the findOne method instead of find.
class UserDao extends UserBaseDao { /** * Returns a user by login * * @param string $login * @return UserBean|null */ public function getUserByLogin($login) { return $this->findOne("login = :login", [ "login" => $login ]); } }
TDBM detects indexes and generates DAO methods for you.
Here is a sample:
CREATE INDEX users_status_idx ON users (status);
The users table has an index on the status column.
Automatically, TDBM will generate a findByStatus method in the UserBaseDao class:
$users = $userDao->findByStatus('on');
But wait, there is more! What about unique indexes?
CREATE UNIQUE INDEX users_login_idx ON users (login);
TDBM will generate a findOneByLogin method:
$user = $userDao->findOneByLogin('alice');
Please note how a unique index generates a findOneBy... method instead of a findBy... method.
Finally, TDBM can also deal with multi-columns indexes, or indexes on foreign keys:
CREATE INDEX users_status_country_idx ON users (status, country_id);
This index on both the status and the country_id column will be turned into a findByLoginAndCountry method:
$country = $countryDao->getById(1); $user = $userDao->findByLoginAndCountry('on', $country);
Notice how the parameter passed for the for foreign key is a bean and not an ID.
So what if I want to get the name of the country in which the first user is located?
// Let's get the user bean $userBean = $userDao->getById(1); // Let's get the country bean $countryBean = $userBean->getCountry(); // Let's display the country name echo $countryBean->getName();
Of course, there is also a setter:
$userBean->setCountry($countryBean);
Notice how you set an object rather than an ID.
Ok. What, now, if I want to find a list of users from a particular country?
// Let's get the country bean $countryBean = $countryDao->getById(1); // Let's get the users from that country $userBeans = $countryBean->getUsers();
TDBM can automatically detect pivot tables in your data model.
// Getter $rolesBean = $userBean->getRoles(); // Adder $userBean->addRole($roleBean); // Remover $userBean->removeRole($roleBean); // Check existence $hasRole = $userBean->hasRole($roleBean);
Pivot tables have no DAO and no Beans associated.
Many to many relationships are symmetrical. Therefore, you will find the same methods in the RoleBean class:
// Getter $usersBean = $roleBean->getUsers(); // Adder $userBean->addUser($roleBean); // Remover $userBean->removeUser($roleBean); // Check existence $hasUser = $roleBean->hasUser($roleBean);
Unlike in Doctrine, TDBM does not need to have a notion of owning and inverse side of a many to many relationship.
$roleBean = new RoleBean("Admin"); $rightBean = new RightBean("CAN_ACCESS_EVERYTHING"); $roleBean->addRight($rightBean); foreach ($rightBean->getRoles() as $role) { echo $role->getName()."\n"; } // Will display "Admin".
Works in TDBM only! Do not try the code above with a Doctrine entity... :)
TDBM writes joins for you!
class UserDao extends UserBaseDao { public function getUsersByCountryName($countryName) { // Behold the magic! return $this->find( "country.name LIKE :country", [ 'country' => $countryName.'%' ] ); } }
Here, we called the find method passing a filter on the name column of the country table.
Magic provided by MagicQuery. Works with any table of your data model (not only adjacent tables).
Most of the time, of course, you will not pass the name of the country but the ID of the country. Actually, using TDBM you can just pass the object. Have a look!
class UserDao extends UserBaseDao { public function getUsersByCountry(CountryBean $countryBean) { return $this->find($countryBean); } }
So now, what if I want to find what rights the user "Robert Marley" has?
class RoleDao extends RoleBaseDao { public function getRolesForUser(UserBean $user) { // Shazam! return $this->find($user); } }
TDBM finds the shortest path between 2 tables and performs the join.
Syntactic sugar: if your filter is only made of "=" and "AND" statements, you can use the shortcut "array" syntax in your queries. Here, we filter a products table by category_id and status:
class ProductDao extends ProductBaseDao { public function getByCategoryAndStatus( int $category_id, int $status) { return $this->find([ 'category_id' => $category_id, 'status' => $status, ]); } }
You can get your results in a specific order using the third parameter of the find method:
class UserDao extends UserBaseDao { public function getUsersByAlphabeticalOrder() { return $this->find(null, [], 'name ASC'); } }
The result of a find or findAll method is an iteratable object. This object can be paginated (it implements the Porpaginas interface).
If you want to limit the number of records returned, or start at a given offset, you can use the take method.
$users = $userDao->findAll(); $page = $users->take(0, 10); // Iterate only the first 10 records foreach ($page as $user) { // Do stuff... }
There are a number of very useful methods on result sets and pages:
$users = $userDao->findAll(); echo "Total count: ".$users->count(); $page = $users->take(0, 10); echo "Page count: ".$page->count(); echo "Total count: ".$page->totalCount(); // Cast to a simple array $pageAsArray = $page->toArray();
You can get the first item of a result set with the first method:
$users = $userDao->findAll(); $firstUser = $users->first();when you use the first method, TDBM appends LIMIT 1 OFFSET 0 to your query to fetch only one row. Also, the result set contains no result, null is returned.
You can directly apply a function to all elements of a result set:
$users = $userDao->findAll(); $userNames = $users->map(function(UserBean $user) { return $user->getName(); }); // $userNames is an array of names!
By default, result sets can be accessed as arrays:
$users = $userDao->findAll(); echo $users[0]->getName;
For very large datasets, this would fill memory. Instead, use the cursor mode (fifth parameter of find method):
class UserDao extends UserBaseDao { public function getUsersInCursorMode() { return $this->find(null, [], null, [], TDBMService::MODE_CURSOR); } }
Sometimes, you want all users and you know you will need to fetch all countries associated. In SQL, this can be done in one query.
$users = $userDao->findAll(); foreach ($users as $user) { echo $user->getCountry()->getName(); }
By default, each time you call the getCountry() method, TDBM will perform a query in database if the country has not been fetched yet. It can be costly.
Good news! You can ask TDBM to fetch the countries table while fetching the users (in the same query). This is the 4th parameter of the find method:
class UserDao extends UserBaseDao { public function getUsersOptimized() { return $this->find(null, [], null, [ 'country' ]); } }
In a database, inheritance can be represented this way:
TDBM will generate an inheritance hierarchy between the beans of these tables:
UserBean > UserBaseBean > ContactBean > ContactBaseBean > PersonBean > PersonBaseBean > AbstractTDBMObject
This means you can do things like:
// The email column belongs to the contact table // not the user. // But this is ok! $userBean->setEmail('foo@bar.com');
Also, you only save the bean once and all three tables are changed.
Serialization works this way:
{ "id": 4, "name": "Bill Shakespeare", "createdAt": "2015-10-24T13:57:13+00:00", "email": "bill@shakespeare.com", "country": { "id": "2", "label": "UK" }, "roles": [ { "id": 2, "name": "Writers" } ] }