-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME
518 lines (400 loc) · 18.2 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
database - v1.11.0
--------------------------------------------------------------------------------
This library provides a really simple PDO database connection wrapper in
addition to providing several database abstraction mechanisms for representing
database entities and schemas. This package is designed to eliminate boilerplate
associated with connecting to the database and managing credentials. Note that
this library does not provide fine-tuned query abstractions.
While targeting PDO lets the library use any database engine, the abstractions
code has been tested and developed for use with MySQL. That isn't to say it
won't necessarily work with another database engine though. You might have more
of a problem with compatibility with the Entity framework classes.
Primary authors:
Roger Gee <roger.gee@tulsalibrary.org>
--------------------------------------------------------------------------------
Installing
This library is available as a composer package. Require 'tccl/database' in your
composer.json file and then install.
--------------------------------------------------------------------------------
Classes
TCCL\Database\DatabaseConnection
Wraps PDO to represent a database connection
TCCL\Database\Entity
Provides an abstraction for managing a single entity
TCCL\Database\EntityInsertSet
Provides an abstraction for inserting multiple Entity objects
TCCL\Database\EntityList
Provides an abstraction for manipulating lists of database entities
TCCL\Database\Schema
Provides abstraction for defining and installing schemas
TCCL\Database\ReflectionEntity
Provides an Entity abstraction where metadata is obtained from doc comments
TCCL\Database\ReflectionEntityTrait
Used with ReflectionEntity
--------------------------------------------------------------------------------
Usage
[DatabaseConnection]
Use a DatabaseConnection instance to manage a database connection. The
connection parameters are to be stored in the $GLOBALS superglobal. The bucket
under this array is arbitrary and may be arbitrarily nested. The structure of
the bucket is an indexed array with three parts like:
<?php
$dbconfig = array(
'mysql:host=localhost;dbname=db', // connection string
'user', // database user
'password', // database user password
);
When initializing a database connection, pass in the keys that index $GLOBALS to
get to the database array bucket. For example, to load the database info from
the global variable from the last example (i.e. $dbconfig):
<?php
$conn = new \TCCL\Database\DatabaseConnection('dbconfig');
If you've nested the array down, specify a parameter list to the constructor:
<?php
define('CONFIG','my-app');
$GLOBALS['a']['b'][CONFIG]['dbinfo'] = array( /* ... */ );
$conn = new \TCCL\Database\DatabaseConnection('a','b',CONFIG,'dbinfo');
It may be useful to extend DatabaseConnection and implement a singleton
pattern. The constructor of your subclass should take care of passing in the
correct keys to its parent constructor. Note that a singleton pattern is not
entirely necessary since the DatabaseConnection caches PDO instances in a static
class property.
Provided methods:
function query($query,$args = null /* ... */)
Runs a query as a prepared statement. Args may be a single array
specifying all the query parameters or the first argument in a parameter
list. Alternatively no $args can be specified if the parameter is
omitted, though you might just use rawQuery() for performance.
Example:
$conn->query('SELECT * FROM table WHERE a = ? and b = ?',$a,$b);
OR
$conn->query('SELECT * FROM table WHERE a = ? and b = ?',[$a,$b]);
function rawQuery($query)
Runs a query directly (no filtering). Depending on the underlying
driver, you may be able to run more than one statement in a single
query string.
function prepare($query)
Wraps PDO::prepare().
function getPDO()
Gets the underlying PDO connection instance.
function beginTransaction()
Wraps PDO::beginTransaction(); however the transaction is reference
counted so multiple contexts can pretend to have a transaction (they
really just share the same transaction).
function endTransaction()
Wraps PDO::endTransaction(); reference counting is employed in tandem
with DatabaseConnection::beginTransaction().
function rollback()
Wraps PDO::rollback(); this resets the reference counter. Therefore it's
the responsiblity of the caller to properly unwind each context (hint:
throwing an Exception is typically a good way to implement this).
function lastInsertId()
Wraps PDO::lastInsertId().
[Entity]
Use the Entity class to define a data model in an application with an
object-oriented interface. This model should map to a single core entity in the
database schema, though you can write hooks to handle other entities as
well. The Entity class is abstract: to use the class, you simply have to define
a constructor that calls the parent, Entity constructor. Then you register
fields on the Entity. The fields you register become dynamic properties on the
object, and you can define filters and aliases for fields as well. The Entity
class can write queries for you to fetch (i.e. SELECT), INSERT and UPDATE an
entity. However you can override queries to provide you own implementations for
more advanced scenarios. This is useful for providing virtual fields which may
be the result of JOINing on other tables.
For example, consider the following schema:
CREATE TABLE person (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(128),
nationality VARCHAR(128),
dob DATETIME,
favorite_integer INT,
PRIMARY KEY (id)
);
An Entity that models this might look like:
<?php
class Person extends Entity {
public function __construct($id = null) {
$keys = ['id' => $id];
parent::__construct(get_db_conn(),'person',$keys);
$this->registerField('id','personId',null,'intval');
$this->registerField('name');
$this->registerField('nationality',null,'American');
$this->registerField('dob','dateOfBirth',null,'date_create');
$this->registerField('favorite_integer','favInt',null,'intval');
}
/**
* Overrides Entity::processCommitFields().
*/
protected function processCommitFields(array $fields) {
if (isset($fields['dob'])) {
$fields['dob'] = $fields['dob']->format('Y-m-d H:i:s');
}
}
}
Every entity constructor receives a DatabaseConnection object, a table name and
a list of keys. Optionally you can specify a fourth parameter that indicates the
create state of an Entity. The $keys array specifies which fields act as keys
when fetching an entity or creating a new entity. It's worth noting that a key
name of 'id' has a special meaning for when a new entity is INSERTed into the
database and should typically be used. The key value can be null, which
indicates the new Entity is a candidate for creation. Otherwise the object will
attempt a lookup on an existing entity.
The registerField() method defines the result fields from the fetch query. By
default you can define this or some subset of the table fields. However if you
override the query by overriding getFetchQuery(), you can define a field for a
subset of that query's result set. This method also allows you to define
aliases; note that these aliases are only used in the object and not in any
queries. If you use aliases in a custom query, they will be used for the field
name just like with any PDO result set. Optionally you can define a default
value for a field (useful for new entities) and a filter callback for
transforming the string database result into some other PHP type. The filter
callback is only used when fetching an existing entity.
In the example, the 'dateOfBirth' field is an alias for the table field
'dob'. The actual field on the Entity object will be a DateTime instance,
created by filtering the date string through the date extension's date_create()
function.
Fields are accessed like public properties on Entity objects. If you specified
an alias, you must use the alias name. Otherwise, the table field name is
valid. Here's an example:
<?php
$id = 5; // assume there is person with id=5
$person = new Person($id);
var_dump($person->name);
This example looks up the name of the person entity with id=5. An Entity
lazy-loads its fields, so the actual query doesn't happen until first access.
When writing a field, the Entity marks that field as dirty. Then when you commit
the entity, only the dirty fields are used to INSERT/UPDATE the entity. The
Entity keeps track of whether it exists or not, deduced from the $keys parameter
to the Entity constructor. You can query whether an Entity exists via the
exists() method.
The following example creates a new Entity:
<?php
$bob = new Person;
$bob->name = 'Bob';
$bob->favInt = 56;
$bob->dateOfBirth = new DateTime('1993-09-14 00:00:00');
$bob->commit();
var_dump($bob->id);
Since the 'id' field is handled specially by the functionality, it gets the
insert ID from the underlying query result. The commit() method returns true if
the Entity was created or updated. Be careful with this, since it may return
false if an update/insert was empty.
Special features:
- Call $entity->setUpdateOnly(true) to prevent accidental creation of an
entity having keys that do not map to an existing entity. This can also be
prevented by calling exists() to make sure the entity exists.
- An associative array of the fields can be retrieved via
$entity->getFields(). A flag to this method controls whether the keys are
the property names (i.e. includes aliases) or the table field names.
- Call $entity->invalidate() to forget about fetched field values. This
means the next access will perform a query and overwrite all fields.
- Class override API:
- function getFetchQuery(array &$values)
Overrides the fetch query; you return the query and write query
parameters to $values. Use getKeys() to get the key bucket instead
of accessing object properties. Also, getKeyString() can be used to
get an SQL snippet suitable for injecting into a WHERE clause.
- function processFetchResults(array &$fetches)
Process the results of a fetch before they are applied to the
object. Values are read from $fetches and written back to $fetches
as well. Note: the keys in $fetches are the table field names, not
the aliases.
- function processCommitFields(array $fields)
Like processFetchResults() but for values used to commit an
Entity. Due to the implementation, each bucket in $fields is a
reference variable, so you can treat $fields as if it was passed by
reference (when writing array fields). Note: the keys in $fetches
are the table field names, not the aliases.
- function preCommit($insert)
Used to perform a custom hook before a commit has been
processed. The $insert flag determines if the query mode is INSERT
or UPDATE. Note: this method is called in a database transaction
context.
- function postCommit($insert)
Like preCommit() but executed after the entity was committed. If an
'id' was set due to an INSERT, it is guarenteed to be ready for
consumption by this method.
NOTE: preCommit() and postCommit() are executed for empty updates but
not for empty inserts or inserts blocked by setUpdateOnly().
[EntityInsertSet]
This class provides a convenience abstraction to insert multiple entities at
once. However it is not possible to reliably determine the insert IDs this way.
[EntityList]
This class represents a list of entities. In this case, entities are not
represented individually. Instead, they are managed as a group. The list
provides efficient ways to manipulate the list including add, modify and delete
items.
A PHP array type is used to represent an individual list item. The schema for
the entity type is denoted using doc comments or arguments passed into the
constructor.
Example:
/**
* @table fruit
* @key id
* @field[] name:fruitName:
* @field[] calories::intval
*/
class FruitList extends EntityList {
public function __construct() {
parent::__construct(Database::getConnection());
}
}
An entity list can also contain one or more filters used to narrow the list of
entities (e.g. using a subkey). Filters are SQL fragments utilized when updating
or deleting entities in the list. Filters are NOT considered when creating new
entities: you must make sure to assign appropriate fields. While you can specify
your filter as a single fragment, you can also specify multiple fragments which
will be combined in conjunction.
Filter fragments can have bound parameters. These MUST be positional bound
parameters, indicated via a '?' symbol in the fragment. You can assign bound
parameters in your constructor using the setFilterVariables() method. Note that
the set of filter variables span each fragment in order.
Example: consider a list of employees grouped by department:
/**
* @table employee
* @key id
* @field[] name::
* @field[] title::
* @field[] start_date:startDate:
* @filter[] department = ?
*/
class EmployeeList extends EntityList {
public function __construct($department) {
parent::__construct(Database::getSingleton());
$this->setFilterVariables([
$department,
]);
}
}
[ReflectionEntity]
Works like Entity but uses reflection to gather table/field metadata. In this
way, you do not register individual fields but supply doc comments that denote
the entity fields/table info.
When you subclass a ReflectionEntity, you must use the ReflectionEntityTrait in
the same class.
/**
* Represents a 'fruit' entity.
*
* @table fruit_record
*/
class Fruit extends ReflectionEntity {
use ReflectionEntityTrait;
/**
* The entity ID.
*
* @field id
* @key
*/
private $id;
/**
* The fruit's common name.
*
* @field common_name
*/
private $name;
/**
* The fruit's scientific name.
*
* @field scientific_name
*/
private $scientificName;
/**
* The number of calories.
*
* @field calories
*/
private $calories;
public function __construct($id = null) {
$this->id = $id;
$db = /* Get DatabaseConnection instance... */;
parent::__construct($db);
}
}
You can inherit ReflectionEntity types. The ReflectionEntity will inherit
schema. If schema are specified in both parent and child classes, then the
functionality will merge. The most-derived class has precedence when it comes to
table names, fields, ETC.
[Schema]
This class provides a way to manage schemas programmatically. Currently, the
functionality only supports creating initial schema, not applying database
updates.
A "Schema" object implements an array interface, so you can denote the schema
using a PHP array. Since the order of table definitions is important, you must
define the tables in the array in the correct order. Table field metadata
(i.e. types, constraints, ETC.) are specified in this array. You should use the
constants defined in class Schema for field sizes and types. Here is an example
that demonstrates all of the capabilities:
$schema = new Schema;
$schema['table'] = [
// Specify primary keys like this. You may specify more than one to get
// a compound primary key
'primary keys' => [
// Field definitions...
// A SERIAL_TYPE gets AUTO_INCREMENT. You may optionally specify a
// "size" and "not null" properties.
'id' => [
'type' => Schema::SERIAL_TYPE,
'not null' => true,
],
],
// Non-constrained fields are specified here.
'fields' => [
// Field definitions...
// Integer types can have a "size".
'thing1' => [
'type' => Schema::INTEGER_TYPE,
'size' => Schema::SIZE_TINY,
],
'thing2' => [
'type' => Schema::INTEGER_TYPE,
'not null' => true,
'default' => 345,
],
// String types may have a "length".
'string1' => [
'type' => Schema::VARCHAR_TYPE,
'not null' => false,
'length' => 1024,
],
// NUMERIC types *MUST* specify "precision" and "scale".
'net_worth' => [
'type' => Schema::NUMERIC_TYPE,
'precision' => 9,
'scale' => 2,
],
'batting_average' => [
'type' => Schema::FLOAT_TYPE,
],
'memoir' => [
'type' => Schema::BLOB_TYPE,
'size' => Schema::SIZE_BIG,
],
],
// Foreign keys are specified like this. Do not specify the foreign
// key field in the "fields" or any other section.
'foreign keys' => [
'other_table_id' => [
// "key" is a single field definition
'key' => [
'type' => DatabaseSchema::INTEGER_TYPE,
],
'table' => 'other_table',
'field' => 'id',
],
],
// Unique keys are specified like this. These reference existing fields.
'unique keys' => [
// UNIQUE keys may be compound.
'unique_thing1' => ['thing1'],
],
// Indexes are specified like this. These reference existing fields.
'indexes' => [
'index_thing2' => ['thing2'],
],
];
To commit the schema, call the "execute" method:
// $conn = ...
$schema->execute($conn);
To test your schema generation, just use the object like a string:
echo "MY SCHEMA: $schema";