Jasny DB adds OOP design patterns to PHP's database extensions.
- Registered connections
- Entity
- Active record
- Data mapper
- Recordset
- Validation
- Lazy loading
- Soft deletion
- Resultset
- Maintainable code
- Code generation
Jasny DB is a data access layer (not a DB abstraction layer) for PHP. It does allow you properly structure your model, while still using the methods and functionality of PHP's native database extensions.
This library is not intended to be installed directly. The Jasny DB library contains design pattern definitions and implementations. It serves as an abstract base for concrete libraries implemented for specific PHP extensions.
- Jasny\DB-MySQL extends mysqli
- Jasny\DB-Mongo extends mongo
- Jasny\DB-REST for datasources implementing REST
Connection objects are use used to interact with a database. Other object must use a connection object do actions like getting getting, saving and deleting data from the DB.
The static Jasny\DB
serves as a factory and
registry for database connections. Registered connections can be
used globally.
To register a connection use the register($name, $connection)
function. To get a registered connection, use the
conn($name)
function. Connections can be removed from the registry using unregister($name|$connection)
.
$db = new Jasny\DB\MySQL\Connection();
Jasny\DB::register('foo');
Jasny\DB::conn('foo')->query();
Jasny\DB::unregister('foo');
The same connection may be registered multiple times under different names.
Connections implementing the Named
interface can register themselves to Jasny\DB
using the useAs($name)
method.
With the getConnectionName()
you can get the name of a connection.
$db = new Jasny\DB\MySQL\Connection();
$db->useAs('foo');
Jasny\DB::conn('foo')->query();
If you only have one DB connection name it 'default', since $name
defaults to 'default'.
$db = new Jasny\DB\MySQL\Connection();
$db->useAs('default');
Jasny\DB::conn()->query();
Instead of using createConnection()
and register()
directly, you may set Jasny\DB::$config
. This static property
may hold the configuration for each connection. When using the conn()
method, Jasny DB will automatically create a
new connection based on the configuration settings.
Jasny\DB::$config = [
'default' => [
'driver' => 'mysql',
'database' => 'database',
'host' => 'localhost',
'username' => 'root',
'password' => 'secure',
'charset' => 'utf8'
],
'external' => [
'driver' => 'rest',
'host' => 'api.example.com',
'username' => 'user',
'password' => 'secure'
]
];
Jasny\DB::conn()->query();
Jasny\DB::conn('external')->get("/something");
Jasny\DB::$drivers
holds a list of Connection
classes with their driver name. The createConnection($settings)
method uses the driver
setting to select the connection class. The other settings are passed to the connection's
constructor.
An entity is a "thing" you want to represent in a database or other data storages. It can be a new article on your blog, a user in your message board or a permission in your rights management system.
The properties of an entity object is a representation of the data. Entities usually also carry business logic.
The setValues()
methods is a a helper function for setting all the properties from an array and works like a
fluent interface.
$foo = new Foo();
$foo->setValues(['red' => 10, 'green' => 20, 'blue' => 30])->doSomething();
Using the new
keyword is reserved for creating a new entity.
When the data of an entity is fetched, the __set_state()
method is used to create the entity. This method sets the
properties of the entity object before calling the constructor.
Enities may be implement the Active Record pattern. Active records combine data and database access in a single object.
An entity can be loaded from the database using the fetch($id)
.
$foo = Foo::fetch(10); // id = 10
$foo = Foo::fetch(['reference' => 'myfoo']);
Objects that implement the ActiveRecord interface have a save()
method for storing the entity in the database.
$foo->save();
$foo->setValues($data)->save();
Entities may be removed from the database using the delete()
method.
$foo->delete();
Optionally soft deletion can be implemented, so deleted entities can be restored.
$foo->undelete();
You may choose to separate database logic from business logic by using a Data Mapper. The Data Mapper is responsible for loading entities from and storing them to their database.
You should either use Data Mappers or Active Record, not both. When using Data Mappers, the entities should not be aware of the database and contain no database code (eg SQL queries).
An entity can be loaded from the database using the fetch($id)
.
$foo = FooMapper::fetch(10); // id = 10
$foo = FooMapper::fetch(['reference' => 'myfoo']);
To store entities a Data Mapper implements the save($entity)
method.
FooMapper::save($foo);
FooMapper::save($foo->setValues($data));
Entities may be removed from the database using the delete($entity)
method.
FooMapper::delete($foo);
Optionally soft deletion can be implemented, so deleted entities can be restored.
FooMapper::undelete($foo);
An entity tends to be a part of a set of data, like a table or collection. If it's possible to load multiple
entities from that set, the Active Record or Data Mapper implement the Recordset
interface.
The fetch()
method returns a single entity. The fetchAll()
method returns multiple enities. fetchList()
loads a list with the id and description as key/value pairs. The count()
method counts the number of entities
in the set.
The fetch methods are intended to support only simple cases. For specific cases you SHOULD add a specific method and not overload the basic fetch methods.
Fetch methods accept a $filter
argument. The filter is an associated array with field name and corresponding
value. Note that the fetch()
methods takes either a unique ID or filter.
A filter SHOULD always return the same or less results that calling the method without a filter.
$foo = Foo::fetch(['reference' => 'zoo']);
$foos = Foo::fetchAll(['bar' => 10]);
$list = Foo::fetchList(['bar' => 10]);
$count = Foo::count(['bar' => 10]);
Optinally filter keys may include an directives. The following directives are supported:
Key | Value | Description |
---|---|---|
"field" | scalar | Field is the value |
"field (not)" | scalar | Field is not the value |
"field (min)" | scalar | Field is equal to or greater than the value |
"field (max)" | scalar | Field is equal to or less than the value |
"field" | array | Field is one of the values in the array |
"field (not)" | array | Field is none of the values in the array |
If the field is an array, you may use the following directives
Key | Value | Description |
---|---|---|
"field" | scalar | The value is part of the field |
"field (not)" | scalar | The value is not part of the field |
"field (any)" | array | Any of the values are part of the field |
"field (all)" | array | All of the values are part of the field |
"field (none)" | array | None of the values are part of the field |
Filters SHOULD be alligned business logic, wich may not directly align to checking a value of a field. A recordset
SHOULD implement a method filterToQuery
which converts the filter to a DB dependent query statement. You MAY
overload this method to support custom filter keys.
It's save to use $_GET
and $_POST
parameters directly.
// -> GET /foos?color=red&date(min)=2014-09-01&tags(not)=abandoned&created.user=12345
$result = Foo::fetchAll($_GET);
An entity represents an element in the model. The metadata holds information about the structure of the entity. Metadata should be considered static as it describes all the entities of a certain type.
Metadata for a class might contain the table name where data should be stored. Metadata for a property might contain the data type, whether or not it is required and the property description.
Jasny DB support defining metadata through annotations by using Jasny\Meta.
/**
* Foo entity
*
* @supportive yes
*/
class Foo
{
/**
* @var string
* @required
*/
public $color;
}
Metadata can be really powerfull in generalizing and abstracting code. However you can quickly fall into the trap of coding through metadata. This tends to lead to code that's hard to read and maintain.
Only use the metadata to abstract widely use functionality and use overloading to implement special cases.
Entities support type casting. This is done based on the metadata. Type casting is implemented by the Jasny\Meta library.
For php internal types normal type juggling is used. Values
aren't blindly casted. For instance casting "foo"
to an integer would trigger a warning and skip the casting.
Casting a value to a model entity that supports Lazy Loading, creates a ghost object. Entities that implement the Active Record pattern or have a Data Mapper, but do not support Lazy Loading are fetched from the database.
Casting to any other type of object will create a new object normally. For instance casting "bar" to Foo
would result
in new Foo("bar")
.
Entities implementing the Validatable interface, can do some basic validation prior to saving them. This includes checking that all required properties have values, checking the variable type matches and checking if values are uniquely present in the database.
Jasny DB supports lazy loading of entities by allowing them to be created as ghost. A ghost only hold a limited set of the entity's data, usually only the identifier. When other properties are accessed it will load the rest of the data.
When a value is casted to an entity that supports lazy loading, a ghost of that entity is created.
Entities that support soft deletion are deleted in such a way that they can restored.
Deleted entities may restored using undelete()
or they can be permanently removed using purge()
.
The isDeleted()
method check whether this document has been deleted.
Fetch methods do not return deleted entities. Instead use fetchDeleted($filter)
to load a deleted entity. Use
fetchAllDeleted($filter)
to fetch all deleted entities from the database.
Not implemented yet
To create maintainable code you SHOULD at least uphold the following rules:
- Don't access the database outside your model classes.
- Use traits or multiple classes to separate database logic (eg queries) from business.
- Keep the number of
if
s limited. Implement special cases by overloading.
SOLID embodies 5 principles principles that, when used together, will make a code base more maintainable over time. While not forcing you to, Jasny DB supports building a SOLID code base.
Methods are kept small and each method is expected to be overloaded by extending the class.
Functionality of Jasny DB is defined in interfaces and defined in traits around a single piece of functionality or design pattern. The use an a specific interface will trigger behaviour. The trait may or may not be used to implement the interface without consequence.
Using the Active Records pattern is considered breaking the Single responsibility principle. Active records tend to combine database logic with business logic a single class.
However this pattern produces code that is more readable and easier to understand. For that reason it remains very popular.
In the end the choice is up to you. Using the Active Record pattern is always optional with Jasny DB. Alternatively you may choose to use Data Mapper for database interaction.
// Active Record
$user = User::fetch(10);
$user->setValues($data);
$user->save();
// Data Mapper
$user = UserMapper::fetch(10);
$user->setValues($data);
UserMapper::save($user);
Present in version 1, but not yet available for version 2