Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/oscarotero/simple-crud
PHP library to provide magic CRUD in MySQL/Sqlite databases with zero configuration
https://github.com/oscarotero/simple-crud
crud database mysql orm sqlite
Last synced: 4 days ago
JSON representation
PHP library to provide magic CRUD in MySQL/Sqlite databases with zero configuration
- Host: GitHub
- URL: https://github.com/oscarotero/simple-crud
- Owner: oscarotero
- License: mit
- Created: 2012-12-27T18:43:21.000Z (almost 12 years ago)
- Default Branch: master
- Last Pushed: 2022-09-07T08:06:50.000Z (over 2 years ago)
- Last Synced: 2024-12-14T08:04:42.630Z (11 days ago)
- Topics: crud, database, mysql, orm, sqlite
- Language: PHP
- Homepage:
- Size: 772 KB
- Stars: 241
- Watchers: 16
- Forks: 58
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# SimpleCrud
[![Build Status](https://travis-ci.org/oscarotero/simple-crud.png?branch=master)](https://travis-ci.org/oscarotero/simple-crud)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/oscarotero/simple-crud/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/oscarotero/simple-crud/?branch=master)PHP library to (Create, Read, Update, Delete) in Mysql/Sqlite databases with zero configuration and some magic.
## Naming conventions:
This library relies in some conventions to avoid configuration.
* Table names SHOULD be in [singular](http://stackoverflow.com/a/5841297) and **camelCase**
* Fields names SHOULD be in **singular** and **camelCase**
* The primary key of all tables MUST be `id`.
* Foreign keys MUST be `[tableName]_id`. For example, `post` table uses `post_id` as foreign key.
* Associative tables MUST use an underscore joining the two tables in alphabetic order. For example, the relationship between `post` and `tag` is `post_tag` but `post` and `category` is `category_post`.## Installation
This package is installable and autoloadable via Composer as [simple-crud/simple-crud](https://packagist.org/packages/simple-crud/simple-crud).
```
$ composer require simple-crud/simple-crud
```## Classes
SimpleCrud has the following classes:
* **Database:** Manage the database connection. Uses internally [Atlas.PDO](https://github.com/atlasphp/Atlas.PDO)
* **Query:** Creates the database queries. SimpleCrud is tested with MySQL and SQLite but due uses [Atlas.Query](https://github.com/atlasphp/Atlas.Query) internally, in theory Postgres and Microsoft SQL should be supported too.
* **Table:** Manages a database table
* **Field:** Manages a database field. Used to format and validate values
* **Row:** To store and modify a row
* **RowCollection:** Is a collection of rows## Usage example
Let's say we have the following database scheme:
```sql
CREATE TABLE "post" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT,
`category_id` INTEGER,
`type` TEXT,FOREIGN KEY(`category_id`) REFERENCES category(id)
);CREATE TABLE `category` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`name` TEXT
);CREATE TABLE `tag` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`name` TEXT
);CREATE TABLE `post_tag` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`tag_id` INTEGER NOT NULL,
`post_id` INTEGER NOT NULL,FOREIGN KEY(`tag_id`) REFERENCES tag(id),
FOREIGN KEY(`post_id`) REFERENCES post(id)
);
```To start, create an instance of `SimpleCrud\Database` passing the `PDO` connection.
```php
use SimpleCrud\Database;$pdo = new PDO($dsn, $username, $password);
$db = new Database($pdo);
//To get any table, use magic properties, they will be instantiated on demand:
$post = $db->post;
```SimpleCrud load the database scheme and detects automatically all relationships between the tables using the naming conventions described above. For example the table "post" has a field called "category_id", so SimpleCrud knows that each post has one category.
**Note:** In production environment, you may want to cache the scheme in order to avoid execute these queries and improve the performance. You can do it in this way:
```php
use SimpleCrud\Scheme\Cache;
use SimpleCrud\Scheme\Mysql;if ($cache->has('db_scheme')) {
$array = $cache->get('db_scheme');
$scheme = new Cache($array);
} else {
$scheme = new Mysql($pdo);
$cache->save('db_scheme', $scheme->toArray());
}$db = new Database($pdo, $scheme);
```## Using the library
### Basic CRUD:
You can interact directly with the tables to insert/update/delete/select data:
Use `ArrayAccess` interface to access to the data using the `id`:
```php
//Get the post id = 3;
$post = $db->post[3];//Check if a row exists
if (isset($db->post[3])) {
echo 'exists';
}//Delete a post
unset($db->post[3]);//Update a post
$db->post[3] = [
'title' => 'Hello world'
];//Insert a new post
$db->post[] = [
'title' => 'Hello world 2'
];//Tables implements the Countable interface
$totalPost = count($db->post);
```### Select by other fields
If you want to select a row by other key than `id`, just use the method `get`:
```php
$post = $db->post->get(['slug' => 'post-slug']);
```### Select or create
Sometimes, you want to get a row or create it if it does not exist. You can do it easily with `getOrCreate` method:
```php
$post = $db->post->getOrCreate(['slug' => 'post-slug']);
```### Rows
A `Row` object represents a database row and is used to read and modify its data:
```php
//get a row by id
$post = $db->post[34];//Get/modify fields values
echo $post->title;$post->title = 'New title';
//Update the row into database
$post->save();//Remove the row in the database
$post->delete();//Create a new row
$newPost = $db->post->create(['title' => 'The title']);//Insert the row in the database
$newPost->save();
```### Queries
A `Query` object represents a database query. SimpleCrud uses magic methods to create queries. For example `$db->post->select()` returns a new instance of a `Select` query in the tabe `post`. Other examples: `$db->comment->update()`, `$db->category->delete()`, etc... Each query has modifiers like `orderBy()`, `limit()`:
```php
//Create an UPDATE query with the table post
$updateQuery = $db->post->update(['title' => 'New title']);//Add conditions, limit, etc
$updateQuery
->where('id = ', 23)
->limit(1);//get the query as string
echo $updateQuery; //UPDATE `post` ...//execute the query and returns a PDOStatement with the result
$PDOStatement = $updateQuery();
```The method `get()` executes the query and returns the processed result of the query. For example, with `insert()` returns the id of the new row:
```php
//insert a new post
$id = $db->post
->insert([
'title' => 'My first post',
'text' => 'This is the text of the post'
])
->get();//Delete a post
$db->post
->delete()
->where('id = ', 23)
->get();//Count all posts
$total = $db->post
->selectAggregate('COUNT')
->get();
//note: this is the same like count($db->post)//Sum the ids of all posts
$total = $db->post
->selectAggregate('SUM', 'id')
->get();
````select()->get()` returns an instance of `RowCollection` with the result:
```php
$posts = $db->post
->select()
->where('id > ', 10)
->orderBy('id ASC')
->limit(100)
->get();foreach ($posts as $post) {
echo $post->title;
}
```If you only need the first row, use the modifier `one()`:
```php
$post = $db->post
->select()
->one()
->where('id = ', 23)
->get();echo $post->title;
````select()` has some interesting modifiers like `relatedWith()` to add automatically the `WHERE` clauses needed to select data related with other row or rowCollection:
```php
//Get the post id = 23
$post = $db->post[23];//Select the category related with this post
$category = $db->category
->select()
->relatedWith($post)
->one()
->get();
```### Query API:
Queries use [Atlas.Query](http://atlasphp.io/cassini/query/) library to build the final queries, so you can see the documentation for all available options.
#### Select / SelectAggregate
Function | Description
---------|------------
`one` | Select 1 result.
`relatedWith(Row / RowCollection / Table $relation)` | To select rows related with other rows or tables (relation added in `WHERE`).
`joinRelation(Table $table)` | To add a related table as `LEFT JOIN`.
`getPageInfo()` | Returns the info of the pagination.
`from` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`columns` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`join` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`catJoin` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`groupBy` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`having` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`orHaving` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`orderBy` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`catHaving` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`where` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`whereSprintf` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`catWhere` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`orWhere` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`orWhereSprintf` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`whereEquals` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`limit` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`offset` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`distinct` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`forUpdate` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`setFlag` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)
`bindValue` | [Atlas.Query Select()](http://atlasphp.io/cassini/query/select.html)#### Update
Function | Description
---------|------------
`relatedWith(Row / RowCollection / Table $relation)` | To update rows related with other rows or tables (relation added in `WHERE`).
`set` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`setFlag` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`where` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`orWhere` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`catWhere` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`orderBy` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`limit` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)
`offset` | [Atlas.Query Update()](http://atlasphp.io/cassini/query/update.html)#### Insert
Function | Description
---------|------------
`orIgnore()` | To ignore silently the insertion on duplicated keys, instead throw an exception.
`set` | [Atlas.Query Insert()](http://atlasphp.io/cassini/query/insert.html)
`setFlag` | [Atlas.Query Insert()](http://atlasphp.io/cassini/query/insert.html)#### Delete
Function | Description
---------|------------
`relatedWith(Row / RowCollection / Table $relation)` | To delete rows related with other rows or tables (relation added in `WHERE`).
`setFlag` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`where` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`orWhere` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`catWhere` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`orderBy` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`limit` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)
`offset` | [Atlas.Query Delete()](http://atlasphp.io/cassini/query/delete.html)### Lazy loads
Both `Row` and `RowCollection` can load automatically other related rows. Just use a property named as related table. For example:
```php
//Get the category id=34
$category = $db->category[34];//Load the posts of this category
$posts = $category->post;//This is equivalent to:
$posts = $db->post
->select()
->relatedWith($category)
->get();//But the result is cached so the database query is executed only the first time
$posts = $category->post;
```This allows make things like this:
```php
$titles = $db->post[34]->tag->post->title;//Get the post id=34
//Get the tags of the post
//Then the posts related with these tags
//And finally, the titles of all these posts
```Use magic methods to get a `Select` query returning related rows:
```php
$category = $db->category[34];//Magic property: Returns all posts of this category:
$posts = $category->post;//Magic method: Returns the query instead the result
$posts = $category->post()
->where('pubdate > ', date('Y-m-d'))
->limit(10)
->get();
```### Solving the n+1 problem
The [n+1 problem](http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue) can be solved in the following way:
```php
//Get some posts
$posts = $db->post
->select()
->get();//preload all categories
$posts->category;//now you can iterate with the posts
foreach ($posts as $post) {
echo $post->category;
}
```You can perform the select by yourself to include modifiers:
```php
//Get some posts
$posts = $db->post
->select()
->get();//Select the categories but ordered alphabetically descendent
$categories = $posts->category()
->orderBy('name DESC')
->get();//Save the result in the cache and link the categories with each post
$posts->link($categories);//now you can iterate with the posts
foreach ($posts as $post) {
echo $post->category;
}
```For many-to-many relations, you need to do one more step:
```php
//Get some posts
$posts = $db->post
->select()
->get();//Select the post_tag relations
$tagRelations = $posts->post_tag()->get();//And now the tags of these relations
$tags = $tagRelations->tag()
->orderBy('name DESC')
->get();//Link the tags with posts using the relations
$posts->link($tags, $tagRelations);//now you can iterate with the posts
foreach ($posts as $post) {
echo $post->tag;
}
```### Relate and unrelate data
To save related rows in the database, you need to do this:
```php
//Get a comment
$comment = $db->comment[5];//Get a post
$post = $db->post[34];//Relate
$post->relate($comment);//Unrelate
$post->unrelate($comment);//Unrelate all comments of the post
$post->unrelateAll($db->comment);
```### Pagination
The `select` query has a special modifier to paginate the results:
```php
$query = $db->post->select()
->page(1)
->perPage(50);$posts = $query->get();
//To get the page info:
$pagination = $query->getPageInfo();echo $pagination['totalRows']; //125
echo $pagination['totalPages']; //3
echo $pagination['currentPage']; //1
echo $pagination['previousPage']; //NULL
echo $pagination['nextPage']; //2
```### Events
SimpleCrud uses [PSR-14 Event Dispatcher](https://www.php-fig.org/psr/psr-14/) to dispatch events. The events are attached to tables allowing to validate data, modify queries, etc.
```php
use SimpleCrud\Events\BeforeSaveRow;
use SimpleCrud\Events\CreateSelectQuery;//Get the event dispatcher
$dispatcher = $db->post->getEventDispatcher();//Assign the BeforeSaveRow event listener
$dispatcher->listen(BeforeSaveRow::class, function (BeforeSaveRow $event) {
$row = $event->getRow();if (!$row->createdAt) {
$row->createdAt = new Datetime();
}
});//Assign a CreateSelectQuery
$dispatcher->listen(CreateSelectQuery::class, function (CreateSelectQuery $event) {
$query = $event->getQuery();//Add automatically a where clause in all selects
$query->where('active = true');
});//Create a new post
$post = $db->post->create(['title' => 'Hello world']);//Save the post, so BeforeSaveRow event is triggered
$post->save();$post->createdAt; //This field was filled and saved
//Select a post, so CreateSelectQuery is triggered and only active posts are selected
$posts = $db->post->select()->get();
```You can provide your own event dispatcher:
```php
$myDispatcher = new Psr14EventDispatcher();$db->post->setEventDispatcher($myDispatcher);
```The available Events are:
* `SimpleCrud\Events\BeforeSaveRow`: Executed before save a row using `$row->save()`.
* `SimpleCrud\Events\BeforeCreateRow`: Executed before create a new row with `$table->create()`.
* `SimpleCrud\Events\CreateDeleteQuery`: Executed on create a DELETE query with `$table->delete()`.
* `SimpleCrud\Events\CreateInsertQuery`: Executed on create a INSERT query with `$table->insert()`.
* `SimpleCrud\Events\CreateSelectQuery`: Executed on create a SELECT query with `$table->select()`.
* `SimpleCrud\Events\CreateUpdateQuery`: Executed on create a UPDATE query with `$table->update()`.### Fields
The purpose of the `SimpleCrud\Fields` classes is to convert the data from/to the database for its usage. For example, in Mysql the format used to store datetime values is "Y-m-d H:i:s", so the class `SimpleCrud\Fields\Datetime` converts any string or `Datetime` instance to this format, and when you select this value, you get a Datetime instance. The available fields are:
* Boolean: To manage boolean values
* Date: To manage date values. Converts the database values to a `Datetime`
* Datetime: To manage datetime values. Converts the database values to a `Datetime`
* Decimal: Converts values to float numbers or NULL
* Field: It's the default field and doesn't transform the value
* Integer: Converts values to integers or NULL
* Json: To store json structures.
* Serializable: To store arrays or any other serializable data structure.
* Set: Manages multiple values. For example: ['red', 'blue', 'green'] will be stored as "red,blue,green" in database.
* Point: Manages geometry points [more info](https://dev.mysql.com/doc/refman/5.7/en/gis-class-point.html)
* Other advanced fields can be found here: https://github.com/oscarotero/simple-crud-extra-fieldsThe Field classes are asigned automatically according with the field type in the database. There are also "special names" that have specific types asigned:
* Integer format will be asigned to any field named `id` or ending by `_id`.
* Datetime format will be asigned to any field named `pubdate` or ending by `At` (for example: `createdAt`, `updatedAt` etc).
* Boolean format will be asigned to any field named `active` or starting by `is` or `has` (for example: `isActived`, `hasContent`, etc)Example:
```php
$post = $db->post->create([
'title' => 'My post',
'text' => 'My post text',
'createdAt' => new Datetime('now'),
'isActive' => true
]);$post->save();
//Use magic properties to get the Field instance
$titleField = $db->post->title;
```### Configuration
You may want to store some database configuration, for example the default language or base path where the assets are stored. To do that, there are the `getConfig` and `setConfig` methods:
```php
$db->setConfig('name', 'value');echo $db->getConfig('name'); //value
```### Localizable fields
If you need to save values in multiple languages, just have to create a field for each language using the language as suffix. For example, to save the title in english (en) and galician (gl), just create the fields `title_en` and `title_gl`.
Then, you have to configure the current language using the `SimpleCrud::ATTR_LOCALE` attribute:
```php
//Set the current language as "en"
$db->setConfig(SimpleCrud::CONFIG_LOCALE, 'en');//Select a post
$post = $db->post[23];//Get the title in the current language
echo $post->title; //Returns the value of title_en//You can access to any languages using the full name:
echo $post->title_en;
echo $post->title_gl;//And assign a diferent value to the current language
$post->title = 'New title in english';
```## Debugging
`SimpleCrud` use internally [Atlas.PDO](http://atlasphp.io/cassini/pdo/) to manage the connection and perform the queries in the database. You can see the documentation for more details.
```php
$db->getConnection()->logQueries(true);//-- Run queries --//
$queries = $db->getConnection()->getQueries();
```## Customization
You can use your own custom classes for tables, rows and row collections:
### Custom Tables
Use `setTableClasses` to assign custom classes to table:
```php
$db = new SimpleCrud\Database($pdo);$db->setTableClasses([
'post' => CustomPost::class,
'comment' => CustomComment::class,
]);$db->post; //Returns an instance of CustomPost
```### FieldFactory
To create field instances, SimpleCrud use the `SimpleCrud\Field\FieldFactory` factory class that you can customize or even replace with your own factory:
```php
use SimpleCrud\Fields\FieldFactory;
use SimpleCrud\Fields\Boolean;$db = new SimpleCrud\Database($pdo);
//Create a factory for your custom field
$factory = new FieldFactory(
Year::class, //Your custom field class name
['integer'], //All fields of type integer will use this class
['year', '/$year/'], //All fields named "year" or matching this regex will use this class
['min' => 2000], //Default config
);$db->setFieldFactory($factory);
//Modify a existing field
$db->getFieldFactory(Boolean::class)->addNames('enabled');//Use it:
$db->post->fields['year']; //returns an instance of Year
$db->post->fields['enabled']; //returns an instance of SimpleCrud\Fields\Boolean
```## Creating your Rows and RowCollections
To define the Rows and RowCollections classes used in a specific table, first create a custom table and use `ROW_CLASS` and `ROWCOLLECTION_CLASS` protected constants to set the class.
```php
namespace MyModels;use SimpleCrud\Table;
class Post extends Table
{
protected const ROW_CLASS = PostRow::class;
protected const ROWCOLLECTION_CLASS = PostRowCollection::class;protected function init()
{
//Insert code to be executed after the instantion
}public function selectLatest()
{
return $this->select()
->orderBy('createdAt DESC')
->limit(10);
}
}
```Now configure the database to use this class for the table `post`:
```php
$db = new SimpleCrud\Database($pdo);
$db->setTableClasses([
'post' => MyModels\Post::class,
]);$latests = $db->post->selectLatest()->get(); //Returns an instance of MyModels\PostRowCollection
foreach ($latests as $post) {
//Instances of MyModels\PostRow
}
```