-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 267 KB
/
index.json
File metadata and controls
1 lines (1 loc) · 267 KB
1
[{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-1","title":"1.1.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/pdo.composer require atlas\/pdo ^2.0 "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-2","title":"1.1.1.2. Instantiation","content":"The easiest way to create a Connection is to use its static new() method, either with PDO connection arguments, or with an actual PDO instance:use Atlas\\Pdo\\Connection; \/\/ pass PDO constructor arguments ... $connection = Connection::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ ... or a PDO instance. $connection = Connection::new($pdo); If you need a callable factory to create a Connection and its PDO instance at a later time, such as in a service container, you can use the Connection::factory() method:use Atlas\\Pdo\\Connection; \/\/ get a callable factory that creates a Connection $factory = Connection::factory('sqlite::memory:'); \/\/ later, call the factory to instantiate the Connection $connection = $factory(); If you want to make sure certain SQL queries are run at connection time, you can create your own callable factory:use Atlas\\Pdo\\Connection; \/\/ define a callable factory that creates a Connection and executes a query $factory = function () { $connection = Connection::new('sqlite::memory:'); $connection->exec('...'); return $connection; } \/\/ later, call the factory to instantiate the Connection $connection = $factory(); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-3","title":"1.1.1.3. Calling PDO Methods","content":"The Connection acts as a proxy to the decorated PDO instance, so you can call any method on the Connection that you would normally call on PDO."},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-4","title":"1.1.1.4. Performing Queries","content":"Instead of issuing prepare(), a series of bindValue() calls, and then execute(), you can bind values and get back a PDOStatement result in one call using the Connection perform() method:$stm = 'SELECT * FROM test WHERE foo = :foo AND bar = :bar'; $bind = ['foo' => 'baz', 'bar' => 'dib']; $sth = $connection->perform($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5","title":"1.1.1.5. Fetching Results","content":"The Connection provides several fetch*() methods to help reduce boilerplate code; these all use perform() internally."},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-1","title":"1.1.1.5.1. fetchAffected()","content":"The fetchAffected() method returns the number of affected rows.$stm = \"UPDATE test SET incr = incr + 1 WHERE foo = :foo AND bar = :bar\"; $bind = ['foo' => 'baz', 'bar' => 'dib']; $rowCount = $connection->fetchAffected($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-2","title":"1.1.1.5.2. fetchAll()","content":"The fetchAll() method returns a sequential array of all rows; each row is an associative array where the keys are the column names.$stm = 'SELECT * FROM test WHERE foo = :foo AND bar = :bar'; $bind = ['foo' => 'baz', 'bar' => 'dib']; $result = $connection->fetchAll($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-3","title":"1.1.1.5.3. fetchColumn()","content":"The fetchColumn() method returns a sequential array of the first column from all rows.$result = $connection->fetchColumn($stm, $bind); You can choose another column number with an optional third argument (columns are zero-indexed):\/\/ use column 3 (i.e. the 4th column) $result = $connection->fetchColumn($stm, $bind, 3); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-4","title":"1.1.1.5.4. fetchGroup()","content":"The fetchGroup() method is like fetchUnique() except that the values aren't wrapped in arrays. Instead, single column values are returned as a single dimensional array and multiple columns are returned as an array of arrays.$result = $connection->fetchGroup($stm, $bind, $style = PDO::FETCH_COLUMN) Set $style to PDO::FETCH_NAMED when values are an array (i.e. there are more than two columns in the select)."},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-5","title":"1.1.1.5.5. fetchKeyPair()","content":"The fetchKeyPair() method returns an associative array where each key is the first column and each value is the second column$result = $connection->fetchKeyPair($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-6","title":"1.1.1.5.6. fetchObject()","content":"The fetchObject() method returns the first row as an object of your choosing; the columns are mapped to object properties. An optional 4th parameter array provides constructor arguments when instantiating the object.$result = $connection->fetchObject($stm, $bind, 'ClassName', ['ctor_arg_1']); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-7","title":"1.1.1.5.7. fetchObjects()","content":"The fetchObjects() method returns an array of objects of your choosing; the columns are mapped to object properties. An optional 4th parameter array provides constructor arguments when instantiating the object.$result = $connection->fetchObjects($stm, $bind, 'ClassName', ['ctor_arg_1']); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-8","title":"1.1.1.5.8. fetchOne()","content":"The fetchOne() method returns the first row as an associative array where the keys are the column names.$result = $connection->fetchOne($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-9","title":"1.1.1.5.9. fetchUnique()","content":"The fetchUnique() method returns an associative array of all rows where the key is the value of the first column, and the row arrays are keyed on the remaining column names.$result = $connection->fetchUnique($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-5-10","title":"1.1.1.5.10. fetchValue()","content":"The fetchValue() method returns the value of the first row in the first column.$result = $connection->fetchValue($stm, $bind); "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6","title":"1.1.1.6. Yielding Results","content":"The Connection provides several yield*() methods to help reduce memory usage.Whereas fetch*() methods may collect all the query result rows before returning them all at once, the equivalent yield*() methods generate one result row at a time."},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6-1","title":"1.1.1.6.1. yieldAll()","content":"This is the yielding equivalent of fetchAll().foreach ($connection->yieldAll($stm, $bind) as $row) { \/\/ ... } "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6-2","title":"1.1.1.6.2. yieldColumn()","content":"This is the yielding equivalent of fetchColumn(). foreach ($connection->yieldColumn($stm, $bind) as $val) { \/\/ ... } "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6-3","title":"1.1.1.6.3. yieldKeyPair()","content":"This is the yielding equivalent of fetchKeyPair().foreach ($connection->yieldKeyPair($stm, $bind) as $key => $val) { \/\/ ... } "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6-4","title":"1.1.1.6.4. yieldObjects()","content":"This is the yielding equivalent of fetchObjects().$class = 'ClassName'; $args = ['arg0', 'arg1', 'arg2']; foreach ($connection->yieldObjects($stm, $bind, $class, $args) as $object) { \/\/ ... } "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-6-5","title":"1.1.1.6.5. yieldUnique()","content":"This is the yielding equivalent of fetchUnique().foreach ($connection->yieldUnique($stm, $bind) as $key => $row) { \/\/ ... } "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-7","title":"1.1.1.7. Explicit Bind Value Types","content":"When binding values on a perform()-based query (which includes all fetch*() and yield*() queries), the Connection will use PDO::PARAM_STR as the value type by default.If you want to override the default type, pass the value as an array where the first element is the value and the second element is the PDO param type. For example:\/\/ PDO::PARAM_STR by default $bind['foo'] = 1; \/\/ force to PDO::PARAM_INT $bind['foo'] = [1, PDO::PARAM_INT] Note: If you pass a boolean value and force the type to PDO::PARAM_BOOL, the Connection will store the value as a string '0' for false or string '1' for true. This addresses issues with a long-standing behavior in PDO. "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-8","title":"1.1.1.8. Query Logging","content":"It is sometimes useful to see a log of all queries passing through a Connection. To do so, call its logQueries() method, issue your queries, and then call getQueries().\/\/ start logging $connection->logQueries(true); \/\/ at this point, all query(), exec(), perform(), fetch*(), and yield*() \/\/ queries will be logged. \/\/ get the query log entries $queries = $connection->getQueries(); \/\/ stop logging $connection->logQueries(false); Each query log entry will be an array with these keys: start: when the query started finish: when the query finished duration: how long the query took performed: whether or not the query was actually perfomed; useful for seeing if a COMMIT actually occurred statement: the query statement string values: the array of bound values trace: an exception trace showing where the query was issued "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-8-1","title":"1.1.1.8.1. Logged Statements","content":"When queries are not being logged, Connection::prepare() will return a normal PDOStatement. However, when queries are being logged, Connection::prepare() will return an Atlas\\Pdo\\LoggedStatement instance instead.The LoggedStatement is an extension of PDOStatement, so it works the same way, but it has the added behavior of recording to the log when its execute() method is called."},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-8-2","title":"1.1.1.8.2. Custom Loggers","content":"You may wish to set a custom logger on the Connection. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.class CustomDebugger { public function __invoke(array $entry) : void { \/\/ call an injected logger to record the entry } } $customDebugger = new CustomDebugger(); $connection->setQueryLogger($customDebugger); $connection->logQueries(true); \/\/ now the Connection will send query log entries to the CustomDebugger Note: If you set a custom logger, the Connection will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Connection not show any new entries. "},{"id":"\/dymaxion\/pdo\/connection.html#1-1-1-9","title":"1.1.1.9. Persistent Connections","content":"Unlogged persistent Connection instances are fully supported, via the PDO::ATTR_PERSISTENT option at construction time.Logged persistent Connection instances are almost fully supported. The only exception to full support is that, on calling Connection::prepare(), the returned statement instance (PersistentLoggedStatement) does not honor the PDOStatement::bindColumn() method. All other methods and behaviors are fully supported."},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2","title":"1.1.2. Connection Locator","content":"Some applications may use multiple database servers; for example, one for writes, and one or more for reads. The ConnectionLocator allows you to define multiple Connection objects for lazy-loaded read and write connections. It will create the connections only when they are when called. The Connection creation logic should be wrapped in factory callable."},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-1","title":"1.1.2.1. Instantiation","content":"The easiest way to create a ConnectionLocator is to use its static new () method, either with PDO connection arguments, or with an actual PDO instance:use Atlas\\Pdo\\ConnectionLocator; \/\/ pass PDO constructor arguments ... $connectionLocator = ConnectionLocator::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ ... or a PDO instance. $connectionLocator = ConnectionLocator::new($pdo); Doing so will define the default connection factory for the ConnectionLocator."},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-2","title":"1.1.2.2. Runtime Configuration","content":"Once you have a ConnectionLocator, you can add as many named read and write connection factories as you like:\/\/ the write (master) server $connectionLocator->setWriteFactory('master', Connection::factory( 'mysql:host=master.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #1 $connectionLocator->setReadFactory('slave1', Connection::factory( 'mysql:host=slave1.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #2 $connectionLocator->setReadFactory('slave2', Connection::factory( 'mysql:host=slave2.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #3 $connectionLocator->setReadFactory('slave3', Connection::factory( 'mysql:host=slave3.db.localhost;dbname=database', 'username', 'password' )); "},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-3","title":"1.1.2.3. Getting Connections","content":"Retrieve a Connection from the locator when you need it. This will create the Connection (if needed) and then return it. getDefault() will return the default Connection. getRead() will return a random read Connection; after the first call, getRead() will always return the same Connection. (If no read Connections are defined, it will return the default connection.) getWrite() will return a random write Connection; after the first call, getWrite() will always return the same Connection. (If no write Connections are defined, it will return the default connection.) $read = $connectionLocator->getRead(); $results = $read->fetchAll('SELECT * FROM table_name LIMIT 10'); $readAgain = $connectionLocator->getRead(); assert($read === $readAgain); \/\/ true You can get any read or write connection directly by name using the get() method:$foo = $connectionLocator->get(ConnectionLocator::READ, 'foo'); $bar = $connectionLocator->get(ConnectionLocator::WRITE, 'bar'); "},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-4","title":"1.1.2.4. Locking To The Write Connection","content":"If you call the lockToWrite() method, calls to getRead() will return the write connection instead of the read connection.$read = $connectionLocator->getRead(); $write = $connectionLocator->getWrite(); $connectionLocator->lockToWrite(); $readAgain = $connectionLocator->getRead(); assert($readAgain === $write); \/\/ true You can disable the lock-to-write behavior by calling lockToWrite(false)."},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-5","title":"1.1.2.5. Construction-Time Configuration","content":"The ConnectionLocator can be configured with all its connections at construction time; this can be useful with dependency injection mechanisms. (Note that this requires using the constructor proper, not the static new () method.)use Atlas\\Pdo\\Connection; use Atlas\\Pdo\\ConnectionLocator; \/\/ default connection $default = Connection::factory( 'mysql:host=default.db.localhost;dbname=database', 'username', 'password' ); \/\/ read connections $read = [ 'slave1' => Connection::factory( 'mysql:host=slave1.db.localhost;dbname=database', 'username', 'password' ), 'slave2' => Connection::factory( 'mysql:host=slave2.db.localhost;dbname=database', 'username', 'password' ), 'slave3' => Connection::factory( 'mysql:host=slave3.db.localhost;dbname=database', 'username', 'password' ), ]; \/\/ write connection $write = [ 'master' => Connection::factory( 'mysql:host=master.db.localhost;dbname=database', 'username', 'password' ), ]; \/\/ configure locator at construction time $connectionLocator = new ConnectionLocator($default, $read, $write); "},{"id":"\/dymaxion\/pdo\/connection-locator.html#1-1-2-6","title":"1.1.2.6. Query Logging","content":"As with an individual Connection, it is sometimes useful to log all queries on all connections in the ConnectionLocator. To do so, call its logQueries() method, issue your queries, and then call getQueries() to get back the log entries.\/\/ start logging $connectionLocator->logQueries(true); \/\/ retrieve connections and issue queries, then: $queries = $connectionLocator->getQueries(); \/\/ stop logging $connectionLocator->logQueries(false); Each query log entry will have one added key, connection, indicating which connection performed the query. The connection label will be DEFAULT for the default connection, READ: and the read connection name, or WRITE: and the write connection name. Note: Calling logQueries() will turn logging on and off for all instances in the locator, even if those instances are not \"in hand\" at the moment. That is, you do not have to re-get the instance; logging for each connection will be turned on and off \"at a distance.\" You may wish to set a custom logger on the ConnectionLocator. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.class CustomDebugger { public function __invoke(array $entry) : void { \/\/ call an injected logger to record the entry } } $customDebugger = new CustomDebugger(); $connectionLocator->setQueryLogger($customDebugger); $connectionLocator->logQueries(true); \/\/ now the Connection will send query log entries to the CustomDebugger Note: If you set a custom logger, the Connection will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Connection not show any new entries. "},{"id":"\/dymaxion\/pdo\/upgrade.html#1-1-3","title":"1.1.3. Upgrade Notes","content":"Upgrading from Atlas.Pdo 1.x to 2.x should be painless.The primary difference is that it requires PHP 8.0, given the addition of expanded and stricter typehinting in 2.x.Connection::fetchOne() now returns array|false instead of ?array; if you previously checked for null, check for false instead.Connection::fetchObject() now returns object|false instead of mixed.Connection::fetchValue() now explicitly returns mixed.Connection::fetchAll(), fetchColumn(), fetchGroup(), fetchKeyPair(), fetchObjects(), and fetchUnique() now return array|false instead of array."},{"id":"\/dymaxion\/statement\/getting-started.html#1-2-1","title":"1.2.1. Getting Started","content":"This library provides query statement builders for MySQL, Postgres, SQLite, and Microsoft SQL Server. The statements are independent of any particular database connection, though they work best with PDO, PDO wrappers such asAtlas.Pdo, or query performers such as Atlas.Query."},{"id":"\/dymaxion\/statement\/getting-started.html#1-2-1-1","title":"1.2.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/statement.$ composer require atlas\/statement ^1.0 "},{"id":"\/dymaxion\/statement\/getting-started.html#1-2-1-2","title":"1.2.1.2. Instantiation","content":"Instantiate the relevant Statement object using its static new() method, and pass the name of the database driver to use for identifier quoting, limit clauses, etc:use Atlas\\Statement\\Select; use Atlas\\Statement\\Insert; use Atlas\\Statement\\Update; use Atlas\\Statement\\Delete; $select = Select::new('sqlite'); $insert = Insert::new('sqlite'); $udpate = Update::new('sqlite'); $delete = Delete::new('sqlite'); "},{"id":"\/dymaxion\/statement\/getting-started.html#1-2-1-3","title":"1.2.1.3. Execution","content":"Note that you will need to transfer the Statement query string and bound values to a database connection of your choice to actually execute the query. Here is one example example of how to do so:use PDO; use Atlas\\Statement\\Select; $select = Select::new('sqlite'); \/\/ ... $pdo = new PDO('sqlite::memory:'); $sth = $pdo->prepare($select->getQueryString()); foreach ($select->getBindValueObjects() as $name => $value) { $sth->bindValue($name, $value->getValue(), $value->getType()); } $sth->execute(); Tip: The Atlas.Query package extends this library to add query execution methods directly to Statement objects, thus removing the need to transfer the query string and bound values to a database connection. "},{"id":"\/dymaxion\/statement\/binding.html#1-2-2","title":"1.2.2. Value Binding","content":"You can bind values to a Statement in various ways."},{"id":"\/dymaxion\/statement\/binding.html#1-2-2-1","title":"1.2.2.1. Implicit Inline Binding","content":"Many Statement methods allow for inline binding of values. This means that the provided value will be represented by an auto-generated placeholder name in the query string, and the value itself will be retained for binding into that placeholder at query execution time.For example, given this statement ...$select ->columns('*') ->from('foo') ->where('bar = ', $bar_value); \/\/ binds $bar_value inline ... a subsequent call to getQueryString() will return:SELECT * FROM foo WHERE bar = :_1_1_ Note: The first part of the auto-generated placeholder name will increment each time a new statement is created; the second part will increment for each inline value bound to that statement. If $bar_value is foo-bar, calling getBindValues() will return:[ ':_1_1_' => ['foo-bar', \\PDO::PARAM_STR], ] Note that the placeholder is automatically recognized as a string; the same will be true for nulls, integers, and floats.If you want to explicitly bind the value as some other type, you can pass that type after the value:$select ->columns('*') ->from('foo') ->where('bar = ', $bar_value, \\PDO::PARAM_LOB); If you bind an array inline, the Statement will bind each element separately with its own placeholder, comma-separate the placeholders, and wrap them in parentheses. This makes using an IN() condition very convenient.$bar_value = ['foo', 'bar', 'baz']; \/\/ SELECT * FROM foo WHERE bar IN (:_1_1_, :_1_2_, :_1_3_) $select ->columns('*') ->from('foo') ->where('bar IN ', $bar_value); Finally, if the inline value is itself a Statement, it will be converted to a string via getQueryString() and returned surrounded in parentheses:\/\/ SELECT * FROM foo WHERE bar IN (SELECT baz FROM dib) $select ->columns('*') ->from('foo') ->where('bar IN ', $select->subSelect() ->columns('baz') ->from('dib') ); Note: Any values bound to the sub-statement will be transferred to the main statement. "},{"id":"\/dymaxion\/statement\/binding.html#1-2-2-2","title":"1.2.2.2. \nsprintf() Inline Binding","content":"If you need to bind more than one value into a condition, you can use an sprintf variation of implicit binding. Pass an expression string formatted for sprintf along with the values to bind:\/\/ SELECT * FROM foo WHERE bar BETWEEN :_1_1_ AND :_1_2_ $select ->columns('*') ->from('foo') ->whereSprintf( 'bar BETWEEN %s AND %s', $low_value, $high_value ); Note that you should use only %s in the format string, since it is the placeholder token that will be interpolated into the expression, not the actual value."},{"id":"\/dymaxion\/statement\/binding.html#1-2-2-3","title":"1.2.2.3. Explicit Parameter Binding","content":"You can still use the normal PDO binding approach, where you explicitly set named parameters in conditions, and then bind the values with a separate call:$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValue('bar', $bar_value); ->bindValue('baz', $baz_value); These too will automatically recognize strings, nulls, integers, and floats, and set the approporate PDO parameter type. If you want to explicitly bind the value as some other type, pass an optional third parameter to bindValue():$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValue('bar', $bar_value, \\PDO::PARAM_LOB); ->bindValue('baz', $baz_value); You can also bind multiple values at once ...$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValues([ 'bar' => $bar_value, 'baz' => $baz_value ); ... but in that case you will not be able to explicitly set the parameter types.The automatic binding of array elements, as with implicit inline binding, does not work with explicit parameter binding."},{"id":"\/dymaxion\/statement\/ctes.html#1-2-3","title":"1.2.3. Common Table Expressions","content":"Every Statement supports Common Table Expressions. To add one or more CTEs to a Statement, call its with*() methods:\/\/ WITH cte_1 AS (SELECT ...) $insert->with('cte_1', \"SELECT ...\") \/\/ WITH cte_2 (foo, bar, baz) AS (SELECT ...) $update->withColumns('cte_2', ['foo', 'bar', 'baz'], \"SELECT ...\"); Note: You can use any kind of Statement as a CTE, not just a SELECT. To enable or disable recursive CTEs, call withRecursive():\/\/ enable $select->withRecursive(); \/\/ disable $select->withRecursive(false); Further, you can pass a Statement object instead of a string:$cteSelect = Select::new('sqlite'); $cteSelect->...; $delete->with('cte_3', $cteSelect); Note: Any values bound to the CTE Statement will be transferred to the main Statement. "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-1","title":"1.2.4.1.1. Columns","content":"To add columns to the Select, use the columns() method and pass each column as a variadic argument.\/\/ SELECT id, name AS namecol, COUNT(foo) AS foo_count $select ->columns('id') ->columns('name AS namecol', 'COUNT(foo) AS foo_count'); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-2","title":"1.2.4.1.2. FROM","content":"To add a FROM clause, use the from() method:\/\/ FROM foo, bar AS b $select ->from('foo') ->from('bar AS b'); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-3","title":"1.2.4.1.3. JOIN","content":"(All JOIN methods support inline value binding via optional trailing arguments.)To add a JOIN clause, use the join() method:\/\/ LEFT JOIN doom AS d ON foo.id = d.foo_id $select->join( 'LEFT', 'doom AS d', 'foo.id = d.foo_id' ); You can concatenate onto the end of the most-recent JOIN using the catJoin() method:\/\/ LEFT JOIN doom AS d ON foo.id = d.foo_if AND d.bar = :_1_1_ AND d.baz = :_1_2_ $select ->join( 'LEFT', 'doom AS d', 'foo.id = d.foo_id AND d.bar = ', $bar_value )->catJoin(' AND d.baz = ', $baz_value); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-4","title":"1.2.4.1.4. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)To add WHERE conditions, use the where() method. Additional calls to where() will implicitly AND the subsequent condition.\/\/ WHERE bar > :_1_1_ AND zim >= :_1_2_ AND baz :_1_3_ $select ->where('bar > ', $bar_value) ->where('zim >= ', $zim_value) ->andWhere('baz < ', $baz_value); Use orWhere() to OR the subsequent condition.\/\/ WHERE bar > :_1_1_ OR zim >= :_1_2_ $select ->where('bar > ', $bar_value) ->orWhere('zim >= ', $zim_value) You can concatenate onto the end of the most-recent WHERE condition using the catWhere() method:\/\/ WHERE bar > :_1_1_ OR (foo = 88 AND bar < :_1_2_) $select ->where('bar > ', $bar_value) ->orWhere('(') ->catWhere('foo = 88') ->catWhere(' AND bar < ', $bar_value) ->catWhere(')'); Each of the WHERE-related methods has an sprintf variation as well:\/\/ WHERE bar BETWEEN :_1_1_ AND :_1_2_ \/\/ AND baz BETWEEN :_1_3_ AND :_1_4_ \/\/ OR dib BETWEEN :_1_5_ AND :_1_6_ \/\/ ... $select ->whereSprintf('bar BETWEEN %s AND %s', $bar_low, $bar_high) ->andWhereSprintf('baz BETWEEN %s AND %s', $baz_low, $baz_high) ->orWhereSprintf('dib BETWEEN %s AND %s', $dib_low, $dib_high) ->catWhereSprintf(...); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-4-1","title":"1.2.4.1.4.1. Convenience Equality","content":"There is an additional whereEquals() convenience method that adds a series of ANDed equality conditions for you based on an array of key-value pairs: Given an array value, the condition will be IN (). Given an empty array, the condition will be FALSE (which means the query will return no results). Given a null value, the condition will be IS NULL. For all other values, the condition will be =. If you pass a key without a value, that key will be used as a raw unescaped condition.For example:\/\/ WHERE foo IN (:_1_1_, :_1_2_, :_1_3_) \/\/ AND bar IS NULL \/\/ AND baz = :_1_4_ \/\/ AND zim = NOW() \/\/ AND FALSE $select->whereEquals([ 'foo' => ['a', 'b', 'c'], 'bar' => null, 'baz' => 'dib', 'zim = NOW()', 'gir' => [], ]); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-5","title":"1.2.4.1.5. GROUP BY","content":"To add GROUP BY expressions, use the groupBy() method and pass each expression as a variadic argument.\/\/ GROUP BY foo, bar, baz $select ->groupBy('foo') ->groupBy('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-6","title":"1.2.4.1.6. HAVING","content":"(All HAVING methods support implicit and sprintf() inline value binding.)The HAVING methods work just like their equivalent WHERE methods: having() and andHaving() AND a HAVING condition orHaving() ORs a HAVING condition catHaving() concatenates onto the end of the most-recent HAVING condition havingSprintf() and andHavingSprintf() AND a HAVING condition with sprintf() orHavingSprintf() ORs a HAVING condition with sprintf() catHavingSprintf() concatenates onto the end of the most-recent HAVING condition with sprintf() "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-7","title":"1.2.4.1.7. ORDER BY","content":"To add ORDER BY expressions, use the orderBy() method and pass each expression as a variadic argument.\/\/ ORDER BY foo, bar, baz $select ->orderBy('foo') ->orderBy('bar', 'baz'); By default, results are ordered in ascending order (ASC). To sort in a different order, add the revelant keyword. For example, to sort in descending order:\/\/ ORDER BY foo DESC $select ->orderBy('foo DESC') "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-8","title":"1.2.4.1.8. LIMIT, OFFSET, and Paging","content":"To set a LIMIT and OFFSET, use the limit() and offset() methods.\/\/ LIMIT 10 OFFSET 40 $select ->limit(10) ->offset(40); Alternatively, you can limit by \"pages\" using the page() and perPage() methods:\/\/ LIMIT 10 OFFSET 40 $select ->page(5) ->perPage(10); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-9","title":"1.2.4.1.9. DISTINCT, FOR UPDATE, and Other Flags","content":"You can set DISTINCT and FOR UPDATE flags on the Select like so:$select ->distinct() ->forUpdate(); Each of those methods take an optional boolean parameter to enable (true) or disable (false) the flag.You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL HIGH_PRIORITY flag like so:\/\/ SELECT HIGH_PRIORITY * FROM foo $select ->columns('*') ->from('foo') ->setFlag('HIGH_PRIORITY'); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-1-10","title":"1.2.4.1.10. UNION","content":"To UNION or UNION ALL the current Select with a followup statement, call one the union*() methods:\/\/ SELECT id, name FROM foo \/\/ UNION \/\/ SELECT id, name FROM bar $select ->columns('id', 'name') ->from('foo') ->union() ->columns('id', 'name') ->from('bar'); \/\/ SELECT id, name FROM foo \/\/ UNION ALL \/\/ SELECT id, name FROM bar $select ->columns('id', 'name') ->from('foo') ->unionAll() ->columns('id', 'name') ->from('bar'); "},{"id":"\/dymaxion\/statement\/select.html#1-2-4-2","title":"1.2.4.2. Resetting SELECT Elements","content":"The Select class comes with the following methods to \"reset\" various clauses a blank state. This can be useful when reusing the same statement in different variations (e.g., to re-issue a statement to get a COUNT(*) without a LIMIT, to find the total number of rows to be paginated over). reset() removes all clauses from the statement. resetColumns() removes all the columns to be selected. resetFrom() removes the FROM clause, including all JOIN sub-clauses. resetWhere() removes all WHERE conditions. resetGroupBy() removes all GROUP BY expressions. resetHaving() removes all HAVING conditions. resetOrderBy() removes all ORDER BY expressions. resetLimit() removes all LIMIT, OFFSET, and paging values. resetFlags() removes all flags. resetWith() removes the WITH clause. Resetting only works on the current SELECT being built; it has no effect on statements that are already part of UNION."},{"id":"\/dymaxion\/statement\/select.html#1-2-4-3","title":"1.2.4.3. Subselect Objects","content":"If you want create a subselect, call the subSelect() method:$subSelect = $select->subSelect(); When you are done building the subselect, give it an alias using the as() method; the object itself can be used in the desired condition or expression.The following is a contrived example:\/\/ SELECT * FROM ( \/\/ SELECT id, name \/\/ FROM foo \/\/ WHERE id > :_1_1_ \/\/ ) AS sub_alias \/\/ WHERE LENGTH(sub_alias.name) > :_1_2_ $select ->columns('*') ->from( $select->subSelect() ->columns('id', 'name') ->from('foo') ->where('id > ', $id) ->as('sub_alias') ) ) ->where('LENGTH(sub_alias.name) > ', $length); Other examples include:\/\/ joining on a subselect $select->join( 'LEFT', $select->subSelect()->...->as('sub_alias'), 'foo.id = sub_alias.id', ); \/\/ binding a subselect inline $select->where( 'foo IN ', $select->subSelect()->... ); "},{"id":"\/dymaxion\/statement\/insert.html#1-2-5-1-1","title":"1.2.5.1.1. Into","content":"Use the into() method to specify the table to insert into.$insert->into('foo'); "},{"id":"\/dymaxion\/statement\/insert.html#1-2-5-1-2","title":"1.2.5.1.2. Columns","content":"You can set a named placeholder and its corresponding bound value using the column() method.\/\/ INSERT INTO foo (bar) VALUES (:bar) $insert->column('bar', $bar_value); Note that the PDO parameter type will automatically be set for strings, integers, floats, and nulls. If you want to set a PDO parameter type yourself, pass it as an optional third parameter.\/\/ INSERT INTO foo (bar) VALUES (:bar); $insert->column('bar', $bar_value, \\PDO::PARAM_LOB); You can set several placeholders and their corresponding values all at once by using the columns() method:\/\/ INSERT INTO foo (bar) VALUES (:bar) $insert->columns([ 'bar' => $bar_value, 'baz' => $baz_value ]); However, you will not be able to specify a particular PDO parameter type when doing do.Bound values are automatically quoted and escaped; in some cases, this will be inappropriate, so you can use the raw() method to set column to an unquoted and unescaped expression.\/\/ INSERT INTO foo (bar) VALUES (NOW()) $insert->raw('bar', 'NOW()'); "},{"id":"\/dymaxion\/statement\/insert.html#1-2-5-1-3","title":"1.2.5.1.3. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Insert using the returning() method, specifying columns as variadic arguments.\/\/ INSERT ... RETURNING foo, bar, baz $insert ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/insert.html#1-2-5-1-4","title":"1.2.5.1.4. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ INSERT LOW_PRIORITY INTO foo (bar) VALUES (:bar) $insert ->into('foo') ->column('bar', $bar_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-1","title":"1.2.6.1.1. Table","content":"Use the table() method to specify the table to update.$update->table('foo'); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-2","title":"1.2.6.1.2. Columns","content":"You can set a named placeholder and its corresponding bound value using the column() method.\/\/ UPDATE foo SET bar = :bar $update->column('bar', $bar_value); Note that the PDO parameter type will automatically be set for strings, integers, floats, and nulls. If you want to set a PDO parameter type yourself, pass it as an optional third parameter.\/\/ UPDATE foo SET bar = :bar $update->column('bar', $bar_value, \\PDO::PARAM_LOB); You can set several placeholders and their corresponding values all at once by using the columns() method:\/\/ UPDATE foo SET bar = :bar, baz = :baz $update->columns([ 'bar' => $bar_value, 'baz' => $baz_value ]); However, you will not be able to specify a particular PDO parameter type when doing do.Bound values are automatically quoted and escaped; in some cases, this will be inappropriate, so you can use the raw() method to set column to an unquoted and unescaped expression.\/\/ UPDATE foo SET bar = NOW() $update->raw('bar', 'NOW()'); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-3","title":"1.2.6.1.3. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)The Update WHERE methods work just like their equivalent Select methods: where() and andWhere() AND a WHERE condition orWhere() ORs a WHERE condition catWhere() concatenates onto the end of the most-recent WHERE condition whereSprintf() and andWhereSprintf() AND a WHERE condition with sprintf() orWhereSprintf() ORs a WHERE condition with sprintf() catWhereSprintf() concatenates onto the end of the most-recent WHERE condition with sprintf() "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-4","title":"1.2.6.1.4. ORDER BY","content":"Some databases (notably MySQL) recognize an ORDER BY clause. You can add one to the Update with the orderBy() method; pass each expression as a variadic argument.\/\/ UPDATE ... ORDER BY foo, bar, baz $update ->orderBy('foo') ->orderBy('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-5","title":"1.2.6.1.5. LIMIT and OFFSET","content":"Some databases (notably MySQL and SQLite) recognize a LIMIT clause; others (notably SQLite) recognize an additional OFFSET. You can add these to the Update with the limit() and offset() methods:\/\/ LIMIT 10 OFFSET 40 $update ->limit(10) ->offset(40); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-6","title":"1.2.6.1.6. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Update using the returning() method, specifying columns as variadic arguments.\/\/ UPDATE ... RETURNING foo, bar, baz $update ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/update.html#1-2-6-1-7","title":"1.2.6.1.7. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ UPDATE LOW_PRIORITY foo SET bar = :bar WHERE baz = :_1_1_ $update ->table('foo') ->column('bar', $bar_value) ->where('baz = ', $baz_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-1","title":"1.2.7.1.1. FROM","content":"Use the from() method to specify FROM expression.$delete->from('foo'); "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-2","title":"1.2.7.1.2. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)The Delete WHERE methods work just like their equivalent Select methods: where() and andWhere() AND a WHERE condition orWhere() ORs a WHERE condition catWhere() concatenates onto the end of the most-recent WHERE condition whereSprintf() and andWhereSprintf() AND a WHERE condition with sprintf() orWhereSprintf() ORs a WHERE condition with sprintf() catWhereSprintf() concatenates onto the end of the most-recent WHERE condition with sprintf() "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-3","title":"1.2.7.1.3. ORDER BY","content":"Some databases (notably MySQL) recognize an ORDER BY clause. You can add one to the Delete with the orderBy() method; pass each expression as a variadic argument.\/\/ DELETE ... ORDER BY foo, bar, baz $delete ->orderBy('foo') ->orderBy('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-4","title":"1.2.7.1.4. LIMIT and OFFSET","content":"Some databases (notably MySQL and SQLite) recognize a LIMIT clause; others (notably SQLite) recognize an additional OFFSET. You can add these to the Delete with the limit() and offset() methods:\/\/ LIMIT 10 OFFSET 40 $delete ->limit(10) ->offset(40); "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-5","title":"1.2.7.1.5. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Delete using the returning() method, specifying columns as variadic arguments.\/\/ DELETE ... RETURNING foo, bar, baz $delete ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/dymaxion\/statement\/delete.html#1-2-7-1-6","title":"1.2.7.1.6. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ DELETE LOW_PRIORITY foo WHERE baz = :_1_1_ $delete ->from('foo') ->where('baz = ', $baz_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/dymaxion\/statement\/other.html#1-2-8-1","title":"1.2.8.1. Microsoft SQL Server LIMIT and OFFSET","content":"If the Statement driver is for Microsoft SQL Server ('sqlsrv'), the LIMIT-related methods on the Statement will generate sqlsrv-specific variations of LIMIT ... OFFSET: If only a LIMIT is present, it will be translated as a TOP clause. If both LIMIT and OFFSET are present, it will be translated as an OFFSET ... ROWS FETCH NEXT ... ROWS ONLY clause. In this case there must be an ORDER BY clause, as the offset clause is a sub-clause of ORDER BY. "},{"id":"\/dymaxion\/statement\/other.html#1-2-8-2","title":"1.2.8.2. Identifier Quoting","content":"You can apply identifier quoting as needed by using the quoteIdentifier() method (available on all Statement objects).INSERT and UPDATE statements will automatically quote the column name that is being inserted or updated. No other automatic quoting of identifiers is applied."},{"id":"\/dymaxion\/statement\/other.html#1-2-8-3","title":"1.2.8.3. Table Prefixes","content":"One frequently-requested feature for this package is support for \"automatic table prefixes\" on all statements. This feature sounds great in theory, but in practice it is (1) difficult to implement well, and (2) even when implemented it turns out to be not as great as it seems in theory. This assessment is the result of the hard trials of experience. For those of you who want modifiable table prefixes, we suggest using constants with your table names prefixed as desired; as the prefixes change, you can then change your constants."},{"id":"\/dymaxion\/statement\/other.html#1-2-8-4","title":"1.2.8.4. Statement Formatting","content":"Each Statement attempts to format its query strings nicely, but it still may not look \"nice enough\" in some cases. If you want nicely-formatted SQL, say for logs or for debugging, consider using jdorn\/sql-formatter on the string returned by Statement::getQueryString()."},{"id":"\/dymaxion\/query\/getting-started.html#1-3-1-1","title":"1.3.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/query.$ composer require atlas\/query ^2.0 "},{"id":"\/dymaxion\/query\/getting-started.html#1-3-1-2","title":"1.3.1.2. Instantiation","content":"Given an existing Connection instance from Atlas.Pdo, you can create a query object using the static new() method of the query type:use Atlas\\Pdo\\Connection; use Atlas\\Query\\Select; use Atlas\\Query\\Insert; use Atlas\\Query\\Update; use Atlas\\Query\\Delete; $connection = Connection::new('sqlite::memory:'); $select = Select::new($connection); $insert = Insert::new($connection); $udpate = Update::new($connection); $delete = Delete::new($connection); Alternatively, you can pass an existing PDO instance, which will get wrapped in a Connection for you:use PDO; $pdo = new PDO('sqlite::memory:'); $select = Select::new($pdo); You can also pass PDO construction arguments, in which case a new Connection will be created for you:\/\/ PDO construction arguments $insert = Insert::new('sqlite::memory'); Once you have a Query object, you will then be able to both build the query statement and and perform it through that Connection."},{"id":"\/dymaxion\/query\/execution.html#1-3-2","title":"1.3.2. Query Execution","content":"Because each Query object extends its relevant Atlas.Statement class, the Statement methods are available to build the Query statement.Thus, by using a Query, you can both build and execute the statement with a single object."},{"id":"\/dymaxion\/query\/execution.html#1-3-2-1","title":"1.3.2.1. SELECT","content":"After you build a SELECT statement, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $select->perform(); "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-1-1","title":"1.3.2.1.1. Fetching and Yielding","content":"The Select proxies all fetch*() and yield() method calls to the underlying Connection object via the magic __call() method: fetchAll() : array|false fetchAffected() : int fetchColumn(int $column = 0) : array|false fetchGroup(int $style = PDO::FETCH_COLUMN) : array|false fetchKeyPair() : array|false fetchObject(string $class = 'stdClass', array $args = []) : object|false fetchObjects(string $class = 'stdClass', array $args = []) : array|false fetchOne() : array|false fetchUnique() : array|false fetchValue() : mixed yieldAll() : Generator yieldColumn(int $column = 0) : Generator yieldKeyPair() : Generator yieldObjects(string $class = 'stdClass', array $args = []) : Generator yieldUnique() : Generator For example, to build a query and get back an array of all results:\/\/ SELECT * FROM foo WHERE bar > :_1_1_ $result = $select ->columns('*') ->from('foo') ->where('bar > ', $value) ->fetchAll(); foreach ($result as $key => $val) { echo $val['bar'] . PHP_EOL; } For more information on the fetch*() and yield*() methods, please see the Atlas.Pdo Connection documentation."},{"id":"\/dymaxion\/query\/execution.html#1-3-2-2","title":"1.3.2.2. INSERT","content":"After you build an INSERT statement, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $insert->perform(); "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-2-1","title":"1.3.2.2.1. Last Insert ID","content":"If the database autoincrements a column while performing the query, you can get back that value using the getLastInsertId() method:$id = $insert->getLastInsertId(); Note: You can pass a sequence name as an optional parameter to getLastInsertId(); this may be required with PostgreSQL. "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-2-2","title":"1.3.2.2.2. RETURNING","content":"If you added a RETURNING clause with the returning() method, you can retrieve those column values with the returned PDOStatement:$pdoStatement = $insert->perform(); $values = $pdoStatement->fetch(); \/\/ : array "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-3","title":"1.3.2.3. UPDATE","content":"After you build an UPDATE statement, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $update->perform(); If you added a RETURNING clause with the returning() method, you can retrieve those column values with the returned PDOStatement:$pdoStatement = $update->perform(); $values = $pdoStatement->fetch(); \/\/ : array "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-3-1","title":"1.3.2.3.1. RETURNING","content":"If you added a RETURNING clause with the returning() method, you can retrieve those column values with the returned PDOStatement:$pdoStatement = $update->perform(); $values = $pdoStatement->fetch(); \/\/ : array "},{"id":"\/dymaxion\/query\/execution.html#1-3-2-4","title":"1.3.2.4. DELETE","content":"After you build a DELETE statement, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $delete->perform(); \/\/ : PDOStatement "},{"id":"\/dymaxion\/query\/upgrade.html#1-3-3","title":"1.3.3. Upgrade Notes","content":"PHP 8.0 is now required, given the addition of expanded and stricter typehinting.Statement-building proper has been extracted to the Atlas.Statement package.The 1.x method Query::getStatement() has been renamed to Statement::getQueryString().The 1.x method Query::getBindValues() has been renamed to Statement::getBindValueArrays().When using a subselect as an inline value, you no longer need to call getStatement() or getQueryString(). Indeed, you must not, if you want the bound values to transferred to the receiving query properly.Generated placeholder tokens now have a different format; whereas in 1.x they were composed of a single number (:__1__), they are now composed of two numbers (:_1_1_). The first number corresponds to the number of query instances; the second is the count of bound values on that query instance. If you depend on the placeholder token format, e.g. in your tests, you will need to update your expectations."},{"id":"\/cassini\/orm\/getting-started.html#2-1-1-1","title":"2.1.1.1. Integrations","content":"If you are using Symfony 4, you can get started by installing the Atlas.Symfony bundle.If you are using Slim 3, please see the cookbook recipe for Atlas.Otherwise, read below for the stock installation instructions."},{"id":"\/cassini\/orm\/getting-started.html#2-1-1-2","title":"2.1.1.2. Installation","content":"This package is installable and autoloadable via Composer as atlas\/orm. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/orm\": \"~3.0\" }, \"require-dev\": { \"atlas\/cli\": \"~2.0\" } } (The atlas\/cli package provides the atlas-skeleton command-line tool to help create data-source classes for the mapper system.) Note: If you are using PHPStorm, you may wish to copy the IDE meta file to your project to get full autocompletion on Atlas classes: cp .\/vendor\/atlas\/orm\/resources\/phpstorm.meta.php .\/.phpstorm.meta.php "},{"id":"\/cassini\/orm\/getting-started.html#2-1-1-3","title":"2.1.1.3. Skeleton Generation","content":"Next, you will need to create the prerequsite data-source classes using Atlas.Cli 2.x."},{"id":"\/cassini\/orm\/getting-started.html#2-1-1-4","title":"2.1.1.4. Instantiating Atlas","content":"Now you can create an Atlas instance by using its static new() method and passing your PDO connection parameters:use Atlas\\Orm\\Atlas; $atlas = Atlas::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); Optionally, you may pass a Transaction class name as the final parameter. (By default, Atlas will use an AutoCommit strategy, where transactions have to be managed manually.)use Atlas\\Orm\\Atlas; use Atlas\\Orm\\Transaction\\AutoTransact; $atlas = Atlas::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password', AutoTransact::CLASS ); Alternatively, use the AtlasBuilder if you need to define a custom factory callable, such as for TableEvents and MapperEvents classes.use Atlas\\Orm\\AtlasBuilder; use Atlas\\Orm\\Transaction\\BeginOnRead; $builder = new AtlasBuilder( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ get the ConnectionLocator to set read and write connection factories $builder->getConnectionLocator()->...; \/\/ set a Transaction class (the default is AutoCommit) $builder->setTransactionClass(BeginOnRead::CLASS); \/\/ set a custom factory callable $builder->setFactory(function ($class) { return new $class(); }); \/\/ get a new Atlas instance $atlas = $builder->newAtlas(); "},{"id":"\/cassini\/orm\/getting-started.html#2-1-1-5","title":"2.1.1.5. Next Steps","content":"Now you can use Atlas to work with your database to fetch and persist Record objects, as well as perform other interactions. Define relationships between mappers Fetch Records and RecordSets Work with Records and RecordSets Manage transactions Add Record and RecordSet behaviors Handle events Perform direct lower-level queries Other topics such as custom mapper methods, single table inheritance, automated validation, and custom factory callables for dependency injection "},{"id":"\/cassini\/orm\/relationships.html#2-1-2","title":"2.1.2. Mapper Relationships","content":"You can add to the MapperRelationships inside the relevant define() method, calling one of these relationship-definition methods: oneToOne($field, $mapperClass) (aka \"has one\") oneToOneBidi($field, $mapperClass) for a bidirectional relationship oneToMany($field, $mapperClass) (aka \"has many\") manyToOne($field, $mapperClass) (aka \"belongs to\") manyToOneVariant($field, $typeCol) (aka \"polymorphic association\") manyToMany($field, $mapperClass, $throughField) (aka \"has many through\") The $field will become a field name on the returned Record object.Here is an example:namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\Author; use App\\DataSource\\Summary\\Summary; use App\\DataSource\\Reply\\Reply; use App\\DataSource\\Tagging\\Tagging; use App\\DataSource\\Tag\\Tag; use Atlas\\Mapper\\MapperRelationships; class ThreadRelationships extends MapperRelationships { protected function define() { $this->manyToOne('author', Author::CLASS); $this->oneToOne('summary', Summary::CLASS); $this->oneToMany('replies', Reply::CLASS); $this->oneToMany('taggings', Tagging::CLASS); } } "},{"id":"\/cassini\/orm\/relationships.html#2-1-2-1","title":"2.1.2.1. Relationship Key Columns","content":"By default, in all relationships except many-to-one, the relationship will take the primary key column(s) in the native table, and map to those same column names in the foreign table.In the case of many-to-one, it is the reverse; that is, the relationship will take the primary key column(s) in the foreign table, and map to those same column names in the native table.If you want to use different columns, pass an array of native-to-foreign column names as the third parameter. For example, if the threads table uses author_id, but the authors table uses just id, you can do this:class ThreadRelationships extends MapperRelationships { protected function define() { $this->manyToOne('author', Author::CLASS, [ \/\/ native (threads) column => foreign (authors) column 'author_id' => 'id', ]); } } And on the oneToMany side of the relationship, you use the native author table id column with the foreign threads table author_id column.class AuthorRelationships extends MapperRelationships { protected function define() { $this->oneToMany('threads', Thread::CLASS, [ \/\/ native (author) column => foreign (threads) column 'id' => 'author_id', ]); } } "},{"id":"\/cassini\/orm\/relationships.html#2-1-2-2","title":"2.1.2.2. Composite Relationship Keys","content":"Likewise, if a table uses a composite key, you can re-map the relationship on multiple columns. If table foo has composite primary key columns of acol and bcol, and it maps to table bar on foo_acol and foo_bcol, you would do this:class FooRelationships extends MapperRelationships { protected function define() { $this->oneToMany('bars', Bar::CLASS, [ \/\/ native (foo) column => foreign (bar) column 'acol' => 'foo_acol', 'bcol' => 'foo_bcol', ]); } } "},{"id":"\/cassini\/orm\/relationships.html#2-1-2-3","title":"2.1.2.3. Case-Sensitivity","content":" Note: This applies only to string-based relationship keys. If you are using numeric relationship keys, this section does not apply. Atlas will match records related by string keys in a case-senstive manner. If your collations on the related string key columns are not case sensitive, Atlas might not match up related records properly in memory after fetching them from the database. This is because 'foo' and 'FOO' might be equivalent in the database collation, but they are not equivalent in PHP.In that kind of situation, you will want to tell the relationship to ignore the case of related string key columns when matching related records. You can do so with the ignoreCase() method on the relationship definition.class FooRelationships { protected function define() { $this->oneToMany('bars', Bar::CLASS) ->ignoreCase(); } } With that in place, a native value of 'foo' match to a foreign value of 'FOO' when Atlas is stitching together related records."},{"id":"\/cassini\/orm\/relationships.html#2-1-2-4","title":"2.1.2.4. Simple WHERE Conditions","content":"You may find it useful to define simple WHERE conditions on the foreign side of the relationship. For example, you can handle one side of a many-to-one-variant (aka \"polymorphic association\") by selecting only related records of a particular type.In the following example, a comments table has a commentable_id column as the foreign key value, but is restricted to \"video\" values on a discriminator column named commentable_type.class Video extends Mapper { protected function define() { $this->oneToMany('comments', Comment::CLASS, [ 'video_id' => 'commentable_id' ])->where('commentable_type = ', 'video'); } } (These conditions will be honored by MapperSelect::*joinWith() as well.)"},{"id":"\/cassini\/orm\/relationships.html#2-1-2-5","title":"2.1.2.5. Variant Relationships","content":"The many-to-one-variant relationship is somewhat different from the other relationship types. It is identical to a many-to-one relationship, except that the relationships vary by a type (or \"discriminator\") column in the native table. This allows rows in the native table to \"belong to\" rows in more than one foreign table. The typical example is one of comments that can be created on many different types of content, such as static pages, blog posts, and video links.class CommentRelationships extends MapperRelationships { protected function define() { \/\/ The first argument is the field name on the native record; \/\/ the second argument is the type column on the native table. $this->manyToOneVariant('commentable', 'commentable_type') \/\/ The first argument is the value of the commentable_type column; \/\/ the second is the related foreign mapper class; \/\/ the third is the native-to-foreign column mapping. ->type('page', Page::CLASS, ['commentable_id' => 'page_id']) ->type('post', Post::CLASS, ['commentable_id' => 'post_id']) ->type('video', Video::CLASS, ['commentable_id' => 'video_id']); } } Note that there will be one query per variant type in the native record set. That is, if a native record set (of an arbitrary number of records) refers to a total of three different variant types, then Atlas will issue three additional queries to fetch the related records."},{"id":"\/cassini\/orm\/relationships.html#2-1-2-6","title":"2.1.2.6. Many-To-Many Relationships","content":"The many-to-many relationship retrieves foreign records via an association table (a.k.a. a \"through\" relationship). This is typically modeled as a one-to-many from the native mapper to the related \"through\" mapper, where the \"through\" mapper itself has a many-to-one relationship to the foreign mapper.When setting up a many-to-many relationship, make sure the \"through\" mapper has a many-to-one relationship to both sides of the relationship. For example, given a threads table, related to tags, through a taggings table:class ThreadRelationships extends MapperRelationships { protected function define() { \/\/ the \"through\" relationship that joins threads and tags $this->oneToMany('taggings', Tagging::CLASS); \/\/ the \"foreign\" relationship \"through\" taggings $this->manyToMany('tags', Tag::CLASS, 'taggings'); } } class TaggingRelationships extends MapperRelationships { protected function define() { \/\/ the threads side of the association mapping $this->manyToOne('threads', Thread::CLASS); \/\/ the tags side of the association mapping $this->manyToOne('tags', Tag::CLASS); } } class TagRelationships extends MapperRelationships { protected function define() { \/\/ the \"through\" relationship that joins threads and tags $this->oneToMany('taggings', Tagging::CLASS); \/\/ the \"foreign\" relationship \"through\" taggings $this->manyToMany('threads', Thread::CLASS, 'taggings'); } } "},{"id":"\/cassini\/orm\/relationships.html#2-1-2-7","title":"2.1.2.7. Cascading Deletes","content":"Atlas relationships support various form of cascading deletion. That is, when you delete() a Record, whether directly or via a persist() call, Atlas can automatically modify its related (foreign child) Records as desired, either in memory or at the database. Note: Cascading deletes cannot operate on many-to-one relationships, since that kind of foreign Record is on the parent\/owner side. They only operate on one-to-one and one-to-many foreign records (i.e., the child\/owned side). Note also that cascading deleted operate only on loaded relationships; they cannot operate on Records not already in memory. Call one of the following methods on the relationship definition to set up cascading deletes: onDeleteInitDeleted(): This works in concert with the native database foreign ON DELETE CASCADE constraint. This tells Atlas to presume that the database has deleted the related rows, and automatically re-initializes the foreign Record in memory to a DELETED status. onDeleteSetNull(): When you delete the native Record, all the foreign related Record keys for the relationship will get their values set to NULL in memory. You will will need to actually write the related Records back to the database for the new value to be stored; that happens automatically as part of a persist() operation. onDeleteSetDelete(): When the the native Record is deleted, Atlas will call setDelete() on all the foreign Records in the relationship. This will mark the Records for deletion, but they will not actually be deleted until they become part of a persist() operation (or until you delete the Record yourself). onDeleteCascade(): When the native Record is deleted, Atlas will immediately delete the foreign record at the database. For example, to define a relationship so that related Records are marked for deletion automatically:class FooRelationships extends MapperRelationships { protected function define() { $this->oneToMany('bars', Bar::CLASS, ['foo_id' => 'foo_id']) ->onDeleteSetDelete(); } } When a Foo Record gets deleted, all of the related 'bars' in memory will be marked for deletion as well; the 'bars' will be deleted when they become part of a persist() operation:\/\/ given $foo->bars ... $foo = $atlas->fetchRecord(Foo::CLASS, ['bars']); \/\/ ... calling delete() will delete $foo, and mark the $foo->bars \/\/ for deletion, but will not actually delete $foo->bars from the \/\/ database: $atlas->delete($foo); \/\/ ... whereas calling persist() will also delete $foo and mark \/\/ the $foo->bars for deletion, but then continue to persist the \/\/ related records, thus deleting the $foo->bars marked for deletion: $atlas->persist($foo); "},{"id":"\/cassini\/orm\/reading.html#2-1-3","title":"2.1.3. Fetching Records and RecordSets","content":"Use Atlas to retrieve a single Record, an array of Records, or a collection of Records in a RecordSet, from the database."},{"id":"\/cassini\/orm\/reading.html#2-1-3-1","title":"2.1.3.1. Fetching and Reading a Record","content":"Use the fetchRecord() method to retrieve a single Record. It can be called either by primary key, or with a select() query.\/\/ fetch by primary key thread_id = 1 $threadRecord = $atlas->fetchRecord( Thread::CLASS, '1' ); $threadRecord = $atlas ->select(Thread::CLASS) ->where('thread_id = ', '1') ->fetchRecord(); Tip: The select() method gives you access to all the underlying SQL query methods. See Atlas\\Query for more information. Note: If fetchRecord() does not find a match, it will return null. Once you have a Record, you can access the columns via properties on the Record. Assume a database column called title.echo $thread->title; See also the page on working with Records."},{"id":"\/cassini\/orm\/reading.html#2-1-3-2","title":"2.1.3.2. Fetching An Array Of Records","content":"The fetchRecords() method works the same as fetchRecord(), but returns an array of Records. It can be called either with primary keys, or with a select() query.\/\/ fetch thread_id 1, 2, and 3 $threadRecordSet = $atlas->fetchRecords( Thread::CLASS, [1, 2, 3] ); \/\/ This is identical to the example above, but uses the `select()` variation. $threadRecordSet = $atlas ->select(Thread::CLASS) ->where('thread_id IN ', [1, 2, 3]) ->fetchRecords(); To return all rows, use the select() variation as shown below.\/\/ Use the `select()` variation to fetch all records, optionally ordering the \/\/ returned results $threadRecordSet = $atlas ->select(Thread::CLASS) ->orderBy('date_added DESC') ->fetchRecords(); Tip: The select() method gives you access to all the underlying SQL query methods. See Atlas\\Query for more information. "},{"id":"\/cassini\/orm\/reading.html#2-1-3-3","title":"2.1.3.3. Fetching and Reading A RecordSet","content":"The fetchRecordSet() method works just the same as fetchRecords(), but instead of returning an array of Records, it returns a RecordSet collection. Note: If fetchRecordSet() does not find any matches, it will return an empty RecordSet collection object. To check if the RecordSet contains any Records, call the isEmpty() method on the RecordSet. RecordSets act as arrays of Records. As such, you can iterate over the RecordSet and access the Records individually.\/\/ fetch the top 100 threads $threadRecordSet = $atlas ->select(Thread::CLASS) ->orderBy('thread_id DESC') ->limit(100) ->fetchRecordSet(); foreach ($threadRecordSet as $threadRecord) { echo $threadRecord->title; } See also the page on working with RecordSets."},{"id":"\/cassini\/orm\/reading.html#2-1-3-4","title":"2.1.3.4. Fetching Related Records and RecordSets","content":"Any relationships that are set in the Mapper will appear as null in the Record object. Related data will only be populated if it is explicitly requested as part of the fetch or select.On a fetch*(), load relateds using a third argument: an array specifying which related fields to retrieve.$threadRecord = $atlas->fetchRecord( Thread::CLASS, '1', [ 'author', 'summary', 'replies', 'tags', ] ); $threadRecordSet = $atlas->fetchRecordSet( Thread::CLASS, [1, 2, 3], [ 'author', 'summary', 'replies', 'tags', ] ); When using the select() variation, load relateds using the with() method:$threadRecord = $atlas ->select(Thread::CLASS) ->where('thread_id = ', '1') ->with([ 'author', 'summary', 'replies', 'tags', ]) ->fetchRecord(); $threadRecordSet = $atlas ->select(Thread::CLASS) ->where('thread_id IN ', [1, 2, 3]) ->with([ 'author', 'summary', 'replies', 'tags', ]) ->fetchRecordSet(); The related fields will be populated like so: If the related field was not specified as part of the with specification, it will be null. This indicates there was no attempt to load any related data. If the related field was part of the with specification, but there was no related data to be found at the database, the field will be false (for to-one relationships) or an empty RecordSet (for to-many relationships). If you specify with() on a many-to-many relationship but do not explicitly fetch with the \"through\" relationship, Atlas will automatically and implicitly fetch it for you. Given the above example, that means both \"taggings\" and \"tags\" will appear in the Record. (You can include \"taggings\" in the with specification explicitly if you want to.) "},{"id":"\/cassini\/orm\/reading.html#2-1-3-4-1","title":"2.1.3.4.1. Nested Relationships","content":"Relationship-fetching can be nested as deeply as needed. For example, to fetch the author of each reply on each thread:$threadRecord = $this->atlas ->select(Thread::CLASS) ->where('thread_id = ', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author' ] ]) ->fetchRecord(); Alternatively, you can pass a closure to exercise fine control over the query that fetches the relateds:\/\/ fetch thread_id 1; with only the last 10 related replies in descending order; \/\/ including each reply author $threadRecord = $atlas->fetchRecord(Thread::CLASS, '1', [ 'author', 'summary', 'replies' => function ($selectReplies) { $selectReplies ->limit(10) ->orderBy(['reply_id DESC']) ->with([ 'author' ]); }, ]); "},{"id":"\/cassini\/orm\/reading.html#2-1-3-4-2","title":"2.1.3.4.2. Reading Related Records and RecordSets","content":"Accessing related data works just like accessing Record properties except instead of using a column name, you use the relationship name defined in the MapperRelationships.$threadRecord = $this->atlas ->select(Thread::CLASS) ->where('thread_id = ', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author', ], 'tags', ]) ->fetchRecord(); \/\/ Assume the author table has a column named `last_name` foreach ($threadRecord->replies as $reply) { echo $reply->author->last_name; } If you specify with() on a one-to-one or many-to-one relationship that returns no result, the related field will be populated with false.If you specify with() on a one-to-many relationship that returns no result, the field will be populated with an empty RecordSet collection."},{"id":"\/cassini\/orm\/reading.html#2-1-3-5","title":"2.1.3.5. Returning Data in Other Formats","content":"You can return a Record or a RecordSet as an array rather than a Record or RecordSet object using the getArrayCopy() method.$threadRecord = $atlas->fetchRecord('Thread::CLASS', '1'); $threadArray = $threadRecord->getArrayCopy(); $threadRecordSet = $atlas ->select(Thread::CLASS) ->orderBy(['date_added DESC']) ->fetchRecordSet(); $threadsArray = $threadRecordSet->getArrayCopy(); JSON-encoding Records and RecordSets is trival.$threadJson = json_encode($threadRecord); $threadsJson = json_encode($threadRecordSet); "},{"id":"\/cassini\/orm\/reading.html#2-1-3-6","title":"2.1.3.6. Reading Record Counts","content":"If you use a select() to fetch a RecordSet with a limit() or page(), you can re-use the select to get a count of how many Records would have been returned. This can be useful for paging displays.$select = $atlas ->select(Thread::CLASS) ->with([ 'author', 'summary', 'replies' ]) ->limit(10) ->offset(20); $threadRecordSet = $select->fetchRecordSet(); $countOfAllThreads = $select->fetchCount(); "},{"id":"\/cassini\/orm\/records.html#2-1-4-1","title":"2.1.4.1. Creating and Inserting a Record","content":"Create a new Record using the newRecord() method. You can assign data using properties, or pass an array of initial data to populate into the Record.$thread = $atlas->newRecord(Thread::CLASS, [ 'title' => 'New Thread Title', ]); You can assign a value via a property, which maps to a column name.$date = new \\DateTime(); $thread->date_added = $date->format('Y-m-d H:i:s'); You can insert a single Record back to the database by using the Atlas::insert() method, which will pick the appropriate Mapper for the Record to perform the write.$atlas->insert($thread); Warning: The insert() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. Inserting a Record with an auto-incrementing primary key will automatically modify the Record to set the last-inserted ID.Inserting a Record will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.In the following example, assume a Thread Record has a manyToOne relationship with an Author Record using the author_id column. The relationship is named author. (See the section on relationships for more information.)$author = $atlas->fetchRecord(Author::CLASS, 4); $thread = $atlas->newRecord(Thread::CLASS, [ 'title' => 'New Thread Title', 'author' => $author ] ); \/\/ If the insert is successful, the `author_id` column will automatically be \/\/ set to the Author Record's primary key value. In this case, 4. $atlas->insert($thread); echo $thread->author_id; \/\/ 4 Note: If the Author Record is new, Atlas will NOT automatically insert the new Author and set the foreign key on the new Author Record via the insert() method. This can, however, be achieved using the persist() method. This is discussed later in this chapter. The following will fail.$author = $atlas->newRecord(Author::CLASS, [ 'first_name' => 'Sterling', 'last_name' => 'Archer' ] ); $thread = $atlas->newRecord(Thread::CLASS, [ 'title' => 'New Thread Title', 'author' => $author ] ); \/\/ Insert will not create the related Author Record. Use persist() instead. $atlas->insert($thread); "},{"id":"\/cassini\/orm\/records.html#2-1-4-2","title":"2.1.4.2. Updating an Existing Record","content":"Updating an existing record works the same as insert().\/\/ fetch an existing record by primary key $thread = $atlas->fetchRecord(Thread::CLASS, 3); \/\/ Modify the title $thread->title = 'This title is better than the last one'; \/\/ Save the record back to the database. $atlas->update($thread); Warning: The update() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. As with insert(), foreign keys are also updated, but only for existing related records.$thread = $atlas->fetchRecord(Thread::CLASS, 3); $author = $atlas->fetchRecord(Author::CLASS, 4); \/\/ Modify the author $thread->author = $author; \/\/ Save the record back to the database. $atlas->update($thread); "},{"id":"\/cassini\/orm\/records.html#2-1-4-3","title":"2.1.4.3. Deleting a Record","content":"Deleting a record works the same as inserting or updating.$thread = $atlas->fetchRecord(Thread::CLASS, 3); $atlas->delete($thread); Warning: The delete() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. "},{"id":"\/cassini\/orm\/records.html#2-1-4-4","title":"2.1.4.4. Persisting a Record","content":"If you like, you can persist a Record and all of its loaded relationships (and all of their loaded relationships, etc.) back to the database using the Atlas persist() method. This is good for straightforward relationship structures where the order of write operations does not need to be closely managed.The persist() method will: persist many-to-one relateds loaded on the native Record; persist the native Record by ... inserting the Row for the Record if it is new; or, updating the Row for the Record if it has been modified; or, deleting the Row for the Record if the Record has been marked for deletion using the Record::setDelete() method; persist one-to-one and one-to-many relateds loaded on the native Record. $atlas->persist($record); Warning: The persist() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. As with insert and update, this will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.If a related field is not loaded, it cannot be persisted automatically.Note that whether or not the Row for the Record is inserted\/updated\/deleted, the persist() method will still recursively traverse all the related fields and persist them as well.The delete() method will not attempt to cascade deletion or nullification across relateds at the ORM level. Your database may have cascading set up at the database level; Atlas has no control over this."},{"id":"\/cassini\/orm\/records.html#2-1-4-5","title":"2.1.4.5. Marking Records for Deletion","content":"You may also mark records for deletion and they will be removed from the database as part of persist().$thread = $atlas->fetchRecord(Thread::CLASS, 3); \/\/ Mark the record for deletion $thread->setDelete(); $atlas->persist($thread); You can also mark several related Records for deletion and when the native Record is persisted, they will be deleted from the database.\/\/ Assume a oneToMany relationship between a thread and its replies \/\/ Select the thread and related replies $thread = $atlas->fetchRecord(Thread::CLASS, 3, [ 'replies' ] ); \/\/ Mark each related reply for deletion foreach ($thread->replies as $reply) { $reply->setDelete(); } \/\/ Persist the thread and the replies are also deleted $atlas->persist($thread); "},{"id":"\/cassini\/orm\/records.html#2-1-4-6","title":"2.1.4.6. Adding Many-To-Many Relateds","content":"Given the relationships example, here is how you would add a tag to a thread:\/\/ fetch a thread with its tags $thread = $atlas->fetchRecord(Thread::CLASS, $thread_id, [ 'tags', ] ); \/\/ fetch a collection of all tags $tags = $atlas ->select(Tag::CLASS) ->fetchRecordSet(); \/\/ get a tag by name from the collection $tag = $tags->getOneBy(['name' => $tag_name]); \/\/ add the tag to the thread $thread->tags[] = $tag; \/\/ persist the whole thread record $atlas->persist($thread); Note that you do not need to manage the taggings yourself. At persist() time, Atlas will call setDelete() on any taggings that no longer have an associated tag, and will add new taggings for tags that are not already associated (and add the thread and tag objects to the new tagging object automatically)."},{"id":"\/cassini\/orm\/records.html#2-1-4-7","title":"2.1.4.7. Removing Many-To-Many Relateds","content":"If you delete a many-to-many related record, it will delete that record from the database, not merely disassociate it from the native record.Instead, detach the many-to-many related record. This will remove the \"through\" association automatically at persist() time, leaving the foreign record in the database.For example:\/\/ detach (not delete!) a tag from the thread $thread->tags->detachOneBy(['name' => $tag_name); \/\/ persist the whole thread record $atlas->persist($thread); Note again that you do not need to manage the taggings yourself; Atlas will do so for you."},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-1","title":"2.1.5.1. New RecordSets","content":"Create a new RecordSet using the newRecordSet() method.$threadRecordSet = $atlas->newRecordSet(Thread::CLASS); "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-2","title":"2.1.5.2. Appending Records to a RecordSet","content":"You can append a new Record to an existing RecordSet using appendNew(), optionally passing any data you want to initially populate into the Record:$newThread = $threadRecordSet->appendNew([ 'title' => 'New Title', ]); Additionally, you can append foreign Records to a native Record's relateds.$thread = $atlas->fetchRecord(Thread::CLASS, 1, [ 'comments', ]); $comment = $thread->comments->appendNew([ 'thread' => $thread, 'comment' => 'Lorem ipsum dolor sit amet...' ]); \/\/ insert the new comment directly $atlas->insert($comment); \/\/ or persist the whole thread $atlas->persist($thread); "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-3","title":"2.1.5.3. Array Access","content":"The RecordSet also acts as an array, so you can get\/set\/unset Records by their sequential keys in the RecordSet.\/\/ address the second record in the set $threadRecordSet[1]->title = 'Changed Title'; \/\/ unset the first record in the set unset($threadRecordSet[0]); \/\/ push a new record onto the set $threadRecordSet[] = $atlas->newRecord(Thread::CLASS); "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-4","title":"2.1.5.4. Searching within RecordSets","content":"You can search for Records within an existing RecordSet by their column values:$threadRecordSet = $atlas->select(Thread::CLASS) ->where('published = ', 1) ->fetchRecordSet(); \/\/ returns one matching Record object from the RecordSet, \/\/ or null if there is no match $matchingRecord = $threadRecordSet->getOneBy(['subject' => 'Subject One']); \/\/ returns a new RecordSet of matching Record objects from the RecordSet $matchingRecordSet = $threadRecordSet->getAllBy(['author_id' => '5']); "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-5","title":"2.1.5.5. Detaching Records from RecordSets","content":"You can detach Records from a RecordSet by their column values. This does NOT delete Records from the database; it only detaches them from the RecordSet.\/\/ unsets and returns one matching Record from the Record Set, \/\/ or null if there is no match $detachedRecord = $threadRecordSet->detachOneBy(['subject' => 'Subject One']); \/\/ unsets and returns a new RecordSet of matching Record objects $detachedRecordSet = $threadRecordSet->detachAllBy(['author_id' => '5']); \/\/ unsets and returns a new RecordSet of all Record objects $detachedRecordSet = $threadRecordSet->detachAll(); Note: This only detaches them from the RecordSet; it does not delete them from the database. If you need to delete a Record from the database, see the sections on Marking Records for Deletion and deleting Records. "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-6","title":"2.1.5.6. Marking RecordSets For Deletion","content":"You can mark each Record currently in a RecordSet for deletion by using the setDelete() method:\/\/ mark all current records for deletion $threadRecordSet->setDelete(); Note: If you add another Record to the collection at this point, it will not have been marked for deletion. You might only want to mark some of the Records for deletion:$threadRecordSet->getAllBy(['author_id' => 1])->setDelete(); "},{"id":"\/cassini\/orm\/record-sets.html#2-1-5-7","title":"2.1.5.7. Persisting A RecordSet","content":"You can persist each Record in a RecordSet by calling the Atlas method persistRecordSet():$atlas->persistRecordSet($threadRecordSet); This will insert, update, or delete each Record in the RecordSet as appropriate.After persisting a RecordSet, you can detach all of the deleted records in the set by calling its detachDeleted() method."},{"id":"\/cassini\/orm\/transactions.html#2-1-6","title":"2.1.6. Transactions","content":"Atlas always starts in \"autocommit\" mode, which means that each interaction with the database is its own micro-transaction (cf. https:\/\/secure.php.net\/manual\/en\/pdo.transactions.php)."},{"id":"\/cassini\/orm\/transactions.html#2-1-6-1","title":"2.1.6.1. Manual Transaction Management","content":"You can manage transactions manually by calling these methods on the Atlas object:\/\/ begins a transaction on BOTH the read connection \/\/ AND the write connection $atlas->beginTransaction(); \/\/ commits the transaction on BOTH connections $atlas->commit(); \/\/ rolls back the transaction on BOTH connections $atlas->rollBack(); Once you perform a write operation (insert, update, delete, or persist), Atlas will lock to the write connection. That is, all reads will occur against the write connection for the rest of the Atlas object's lifetime."},{"id":"\/cassini\/orm\/transactions.html#2-1-6-2","title":"2.1.6.2. Other Transaction Strategies","content":"If you find that manually managing transactions proves tedious, Atlas comes with three alternative transaction strategy classes: AutoTransact will automatically begin a transaction when you perform a write operation, then automatically commit that operation, or roll it back on exception. (Note that in the case of persistRecordSet(), each Record in the RecordSet will be persisted within its own transaction.) BeginOnWrite will automatically begin a transaction when you perform a write operation. It will not commit or roll back; you will need to do so yourself. Once you do, the next time you perform a write operation, Atlas will begin another transaction. BeginOnRead will automatically begin a transaction when you perform a write operation or a read operation. It will not commit or roll back; you will need to do so yourself. Once you do, the next time you perform a write or read operation, Atlas will begin another transaction. (As with the manual strategy, the transactions are started on BOTH read and write connections, and each of these will lock to the write connection once a write operation is performed.)To specify which transaction strategy to use, pass it as the last argument to the Atlas static new() call ...use Atlas\\Orm\\Atlas; use Atlas\\Orm\\Transaction\\AutoTransact; \/\/ use a MiniTransaction strategy $atlas = Atlas::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password', AutoTransact::CLASS ); ... or call setTransactionClass() on an AtlasBuilder instance:use Atlas\\Orm\\AtlasBuilder; use Atlas\\Orm\\Transaction\\BeginOnRead; $builder = new AtlasBuilder( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ use a BeginOnRead strategy $builder->setTransactionClass(BeginOnRead::CLASS); \/\/ get a new Atlas instance $atlas = $builder->newAtlas(); "},{"id":"\/cassini\/orm\/behavior.html#2-1-7","title":"2.1.7. Record and RecordSet Behaviors","content":"Atlas makes it easy to add your own behaviors to both Records and RecordSets.It's important to note that the Record and RecordSet objects described below should only be used for very simple behaviors. Any non-trivial domain work may be an indication that you need a domain layer. See the documentation on Domain Models for examples of how you can use Atlas to build a domain layer.For example:namespace App\\DataSource\\Thread; use Atlas\\Mapper\\Record; class ThreadRecord extends Record { \/\/ Format the date_created property public function formatDate($format = 'M jS, Y') { $dateTime = new \\DateTime($this->date_created); return $dateTime->format($format); } } $thread = $atlas->fetchRecord(Thread::CLASS, $id); echo $thread->formatDate(); \/\/ outputs something like `Aug 21st, 2017` The same concept is available for RecordSets using the RecordSet class. In our example ThreadRecordSet.php.namespace App\\DataSource\\Thread; use Atlas\\Mapper\\RecordSet; class ThreadRecordSet extends RecordSet { public function getAllTitles() { $titles = [] foreach ($this as $record) { $titles[$record->thread_id] = $record->title; } return $titles; } } $threads = $atlas->fetchRecordSet(Thread::CLASS, [1, 2, 3]); print_r($threads->getAllTitles()); "},{"id":"\/cassini\/orm\/events.html#2-1-8","title":"2.1.8. Events","content":"There are several events that will automatically be called when interacting with Atlas mappers. Note: These mapper-level events are called in addition to the various table-level events. The insert(), update(), and delete() methods all have 3 events associated with them: a before*(), a modify*(), and an after*(). In addition, there is a modifySelect() event.\/\/ Runs after the Select object is created, but before it is executed modifySelect(Mapper $mapper, MapperSelect $select) \/\/ Runs before the Insert object is created beforeInsert(Mapper $mapper, Record $record) \/\/ Runs after the Insert object is created, but before it is executed modifyInsert(Mapper $mapper, Record $record, Insert $insert) \/\/ Runs after the Insert object is executed afterInsert(Mapper $mapper, Record $record, Insert $insert, PDOStatement $pdoStatement) \/\/ Runs before the Update object is created beforeUpdate(Mapper $mapper, Record $record) \/\/ Runs after the Update object is created, but before it is executed modifyUpdate(Mapper $mapper, Record $record, Update $update) \/\/ Runs after the Update object is executed afterUpdate(Mapper $mapper, Record $record, Update $update, PDOStatement $pdoStatement) \/\/ Runs before the Delete object is created beforeDelete(Mapper $mapper, Record $record) \/\/ Runs after the Delete object is created, but before it is executed modifyDelete(Mapper $mapper, Record $record, Delete $delete) \/\/ Runs after the Delete object is executed afterDelete(Mapper $mapper, Record $record, Delete $delete, PDOStatement $pdoStatement) Here is a simple example with the assumption that the Record object has a validate() method and a getErrors() method. See the section on Adding Logic to Records and RecordSets.namespace Blog\\DataSource\\Posts; use Atlas\\Mapper\\Mapper; use Atlas\\Mapper\\MapperEvents; use Atlas\\Mapper\\Record; \/** * @inheritdoc *\/ class PostsEvents extends MapperEvents { public function beforeUpdate(Mapper $mapper, Record $record) { if (! $record->validate()) throw new \\Exception('Update Error'); } } } And you might have something like this in your code:try { $atlas->update($post); } catch (\\Exception $e) { foreach ($post->getErrors() as $error) { echo $error . '<br\/>'; } } "},{"id":"\/cassini\/orm\/direct.html#2-1-9","title":"2.1.9. Direct Queries","content":"If you need to perform queries directly, additional fetch* and yield* methods are provided which expose the underlying Atlas\\Pdo\\Connection functionality. By using the columns() method, you can select specific columns or individual values. For example:\/\/ an array of IDs $threadIds = $atlas ->select(Thread::CLASS) ->columns('thread_id') ->limit(10) ->orderBy('thread_id DESC') ->fetchColumn(); \/\/ key-value pairs of IDs and titles $threadIdsAndTitles = $atlas ->select(Thread::CLASS) ->columns('thread_id', 'title') ->limit(10) ->orderBy('thread_id DESC') ->fetchKeyPair(); \/\/ etc. See the list of Connection fetch() and yield() methods for more.You can also call fetchRow() or fetchRows() to get Row objects directly from the Table underlying the Mapper."},{"id":"\/cassini\/orm\/direct.html#2-1-9-1","title":"2.1.9.1. Complex Queries","content":"You can use any of the direct table access methods with more complex queries and joins as provided by Atlas.Query:$threadData = $atlas ->select(Thread::CLASS) ->columns('threads.subject', 'authors.name', 's.*') ->join('INNER', 'authors', 'authors.author_id = threads.author_id') ->join('INNER', 'summary AS s', 's.thread_id = threads.thread_id') ->where('authors.name = ', $name) ->orderBy('threads.thread_id DESC') ->offset(2) ->limit(2) ->fetchUnique(); "},{"id":"\/cassini\/orm\/direct.html#2-1-9-2","title":"2.1.9.2. Joining On Defined Relationships","content":"In addition the various JOIN methods provided by Atlas.Query, the MapperSelect also provides joinWith(), so that you can join on a defined relationship and then use columns from that relationship. (The related table will be aliased automatically as the relationship name.)For example, to JOIN with another table as defined in the Mapper relationships:$threadIdsAndAuthorNames = $atlas ->select(Thread::CLASS) ->joinWith('author') ->columns( \"threads.thread_id\", \"CONCAT(author.first_name, ' ', author.last_name)\" ) ->limit(10) ->orderBy('thread_id DESC') ->fetchKeyPair(); You can specify the JOIN type as part of the related name string, in addition to an alias of your choosing:\/\/ specify the join type: $select->joinWith('LEFT author'); \/\/ specify an alternative alias: $select->joinWith('author AS author_alias'); \/\/ specify both $select->joinWith('LEFT author AS author_alias'); Finally, you can pass a callable as an optional third parameter to add \"sub\" JOINs on the already-joined relationship. For example, to find all authors with threads that have the \"foo\" tag on them:$authorsWithThreadsAndTags = $atlas ->select(Author::CLASS) ->joinWith('threads', function ($sub) { $sub->joinWith('taggings', function ($sub) { $sub->joinWith('tag'); }); }) ->where('tag = ', 'foo'); This builds a query similar to the following:SELECT * FROM authors JOIN threads ON authors.author_id = threads.author_id JOIN taggings ON threads.thread_id = taggings.thread_id JOIN tags AS tag ON taggings.tag_id = tag.tag_id WHERE tag = :__1__ Note: Using joinWith() does not select any records from the defined relationship; it only adds a JOIN clause. If you want to select related records, use the with() method. "},{"id":"\/cassini\/orm\/direct.html#2-1-9-3","title":"2.1.9.3. Reusing the Select","content":"The select object can be used for multiple queries, which may be useful for pagination. The generated select statement can also be displayed for debugging purposes.$select = $atlas ->select(Thread::CLASS) ->columns('*') ->offset(10) ->limit(5); \/\/ Fetch the current result set $results = $select->fetchAll(); \/\/ Fetch the row count without any limit or offset $totalCount = $select->fetchCount(); \/\/ View the generated select statement $statement = $select->getStatement(); "},{"id":"\/cassini\/orm\/other.html#2-1-10-1","title":"2.1.10.1. Adding Custom Mapper Methods","content":"Feel free to add custom methods to your Mapper classes, though do be sure that they are appropriate to a Mapper. For example, custom fetch*() methods are perfectly reasonable, so that you don't have to write the same queries over and over:namespace App\\DataSource\\Content; use Atlas\\Mapper\\Mapper; class Content extends Mapper { public function fetchLatestContent(int $count) : ContentRecordSet { return $this ->select() ->orderBy('publish_date DESC') ->limit($count) ->fetchRecordSet(); } } Another example would be custom write behaviors, such as incrementing a value directly in the database (without going through any events) and modifying the appropriate Record in memory:namespace App\\DataSource\\Content; use Atlas\\Mapper\\Mapper; class Content extends Mapper { public function increment(ContentRecord $record, string $field) { $this->table ->update() ->set($field, \"{$field} + 1\") ->where(\"content_id = \", $record->content_id) ->perform(); $record->$field = $this->table ->select($field) ->where(\"content_id = \", $record->content_id) ->fetchValue(); } } "},{"id":"\/cassini\/orm\/other.html#2-1-10-2","title":"2.1.10.2. Single Table Inheritance","content":"Sometimes you will want to use one Mapper (and its underlying Table) to create more than one kind of Record. The Record type is generally specified by a column on the table, e.g. record_type. To do so, create Record classes that extend the Record for that Mapper in the same namespace as the Mapper, then override the Mapper getRecordClass() method to return the appropriate class name.For example, given a Content mapper and ContentRecord ...App\\ DataSource\\ Content\\ Content.php ContentEvents.php ContentRecord.php ContentRecordSet.php ContentRelationships.php ContentRow.php ContentSelect.php ContentTable.php ContentTableEvents.php ContentTableSelect.php ... , you might have the content types of \"post\", \"page\", \"video\", \"wiki\", and so on.App\\ DataSource\\ Content\\ Content.php ContentEvents.php ContentRecord.php ContentRecordSet.php ContentRelationships.php ContentRow.php ContentSelect.php ContentTable.php ContentTableEvents.php ContentTableSelect.php PageContentRecord.php PostContentRecord.php VideoContentRecord.php WikiContentRecord.php A WikiContentRecord might look like this ...namespace App\\DataSource\\Content; class WikiContentRecord extends ContentRecord { } ... and the Content getRecordClass() method would look like this:namespace App\\DataSource\\Content; use Atlas\\Mapper\\Mapper; use Atlas\\Table\\Row; class Content extends Mapper { protected function getRecordClass(Row $row) : Record { switch ($row->type) { case 'page': return PageContentRecord::CLASS; case 'post': return PostContentRecord::CLASS; case 'video': return VideoContentRecord::CLASS; case 'Wiki': return PostContentRecord::CLASS; default: return ContentRecord::CLASS: } } } Note that you cannot define different relationships \"per record.\" You can only define MapperRelationships for the mapper as whole, to cover all its record types.Note also that there can only be one RecordSet class per Mapper, though it can contain any kind of Record."},{"id":"\/cassini\/orm\/other.html#2-1-10-3","title":"2.1.10.3. Automated Validation","content":"You will probably want to apply some sort of filtering (validation and sanitizing) to Row (and to a lesser extent Record) objects before they get written back to the database. To do so, implement or override the appropriate TableEvents (or MapperEvents) class methods for before or modify the insert or update event. Irrecoverable filtering failures should be thrown as exceptions to be caught by your surrounding application or domain logic.For example, to check that a value is a valid email address:namespace App\\DataSource\\Author; use Atlas\\Table\\Row; use Atlas\\Table\\Table; use Atlas\\Table\\TableEvents; use UnexpectedValueException; class AuthorTableEvents extends TableEvents { public function beforeInsert(Table $table, Row $row) : void { $this->assertValidEmail($row->email); } public function beforeUpdate(Table $table, Row $row) : void { $this->assertValidEmail($row->email); } protected function assertValidEmail($value) { if (! filter_var($value, FILTER_VALIDATE_EMAIL) { throw new UnexpectedValueException(\"The author email address is not valid.\"); } } } For detailed reporting of validation failures, consider writing your own extended exception class to retain a list of the fields and error messages, perhaps with the object being validated."},{"id":"\/cassini\/orm\/other.html#2-1-10-4","title":"2.1.10.4. Query Logging","content":"To enable query logging, call the Atlas logQueries() method. Issue your queries, and then call getQueries() to get back the log entries.\/\/ start logging $atlas->logQueries(); \/\/ retrieve connections and issue queries, then: $queries = $connectionLocator->getQueries(); \/\/ stop logging $connectionLocator->logQueries(false); Each query log entry will be an array with these keys: connection: the name of the connection used for the query start: when the query started finish: when the query finished duration: how long the query took statement: the query statement string values: the array of bound values trace: an exception trace showing where the query was issued You may wish to set a custom query logger for Atlas. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.class CustomDebugger { public function __invoke(array $entry) : void { \/\/ call an injected logger to record the entry } } $customDebugger = new CustomDebugger(); $atlas->setQueryLogger($customDebugger); $atlas->logQueries(true); \/\/ now Atlas will send query log entries to the CustomDebugger Note: If you set a custom logger, the Atlas instance will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Atlas instance will not show any new entries. "},{"id":"\/cassini\/orm\/other.html#2-1-10-5","title":"2.1.10.5. Custom Factory Callable","content":"The AtlasBuilder let you specify a custom factory callable to create the dependencies for each Table and Mapper instance. The default factory callable looks like this:\/** * @var string $class A fully-qualified class name. * @return object *\/ function (string $class) { return new $class(); } Although this callable may in future be used for any kind of Table or Mapper dependency, in practice it is currently limited to Events classes.If your Events instances need dependency injection, you can replace the default factory with your own callable; the AtlasBuilder will use it to create any new Events instances. This gives you full control over how the Events objects are instantiated. Note: The base TableEvents and MapperEvents classes have no constructors, so you are free to write your own in your generated Events classes. For example, to use a PSR-11 container to create Events objects:$atlasBuilder = new \\Atlas\\Orm\\AtlasBuilder(...); \/** @var \\Psr\\Container\\ContainerInterface $container *\/ $atlasBuilder->setFactory(function (string $class) use ($container) { return $container->get($class); }); $atlas = $atlasBuilder->newAtlas(); \/\/ Atlas will now use $container to create \/\/ TableEvents and MapperEvents instances. "},{"id":"\/cassini\/orm\/domain.html#2-1-11","title":"2.1.11. Domain Models","content":"You can go a long way with just your persistence model Records. However, at some point you may want to separate your persistence model Records from your domain model Entities and Aggregates. This section offers some suggestions and examples on how to do that."},{"id":"\/cassini\/orm\/domain.html#2-1-11-1","title":"2.1.11.1. Persistence Model","content":"For the examples below, we will work with an imaginary forum application that has conversation threads. The Thread mapper might something like this:namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\Author; use App\\DataSource\\Reply\\Reply; use App\\DataSource\\Summary\\Summary; use App\\DataSource\\Tagging\\Tagging; use Atlas\\Mapper\\Mapper; class Thread extends Mapper { protected function setRelated() { $this->manyToOne('author', Author::CLASS); $this->oneToOne('summary', Summary::CLASS); $this->oneToMany('replies', Reply::CLASS); $this->oneToMany('taggings', Tagging::CLASS); } } (We will leave the other mappers and their record classes for the imagination.)"},{"id":"\/cassini\/orm\/domain.html#2-1-11-2","title":"2.1.11.2. Domain Model Interfaces","content":"At some point, we have decided we want to depend on domain Entities or Aggregates, rather than persistence Records, in our application.For example, the interface we want to use for a Thread Entity in domain might look like this:namespace App\\Domain\\Thread; interface ThreadInterface { public function getId(); public function getSubject(); public function getBody(); public function getDatePublished(); public function getAuthorId(); public function getAuthorName(); public function getTags(); public function getReplies(); } (This interface allows us to typehint the application against these domain- specific Entity methods, rather than using the persistence Record properties.)Further, we will presume a naive domain repository implementation that returns Thread Entities. It might look something like this:namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\Thread; class ThreadRepository { protected $mapper; public function __construct(Thread $mapper) { $this->mapper = $mapper; } public function fetchThread($thread_id) { $record = $this->mapper->fetchRecord($thread_id, [ 'author', 'taggings', 'replies', ]); return $this->newThread($record); } protected function newThread(ThreadRecord $record) { \/* ??? *\/ } } The problem now is the newThread() factory method. How do we convert a persistence layer ThreadRecord into a domain layer ThreadInterface implementation?There are three options, each with different tradeoffs: Implement the domain interface in the persistence layer. Compose the persistence record into the domain object. Map the persistence record fields to domain implementation fields. "},{"id":"\/cassini\/orm\/domain.html#2-1-11-3","title":"2.1.11.3. Implement Domain In Persistence","content":"The easiest thing to do is to implement the domain ThreadInterface in the persistence ThreadRecord, like so:namespace App\\DataSource\\Thread; use Atlas\\Mapper\\Record; use App\\Domain\\Thread\\ThreadInterface; class ThreadRecord extends Record implements ThreadInterface { public function getId() { return $this->thread_id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->date_published; } public function getAuthorId() { return $this->author->author_id; } public function getAuthorName() { return $this->author->name; } public function getTags() { $tags = []; foreach ($this->taggings as $tagging) { $tags[] = $tagging->tag->getArrayCopy(); } return $tags; } public function getReplies() { return $this->replies->getArrayCopy(); } } With this, the ThreadRepository::newThread() factory method doesn't actually need to factory anything at all. It just returns the persistence record, since the record now has the domain interface.class ThreadRepository ... protected function newThread(ThreadRecord $record) { return $record; } Pros: Trivial to implement. Cons: Exposes the persistence layer Record methods and properties to the domain layer, where they can be easily abused. "},{"id":"\/cassini\/orm\/domain.html#2-1-11-4","title":"2.1.11.4. Compose Persistence Into Domain","content":"Almost as easy, but with better separation, is to have a domain layer object that implements the domain interface, but encapsulates the persistence record as the data source. The domain object might look something like this:namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadRecord; class Thread implements ThreadInterface { protected $record; public function __construct(ThreadRecord $record) { $this->record = $record; } public function getId() { return $this->record->thread_id; } public function getTitle() { return $this->record->title; } public function getBody() { return $this->record->body; } public function getDatePublished() { return $this->record->date_published; } public function getAuthorId() { return $this->record->author->author_id; } public function getAuthorName() { return $this->record->author->name; } public function getTags() { $tags = []; foreach ($this->record->taggings as $tagging) { $tags[] = $tagging->tag->getArrayCopy(); } return $tags; } public function getReplies() { return $this->record->replies->getArrayCopy(); } } Now the ThreadRepository::newThread() factory method has to do a little work, but not much. All it needs is to create the Thread domain object with the ThreadRecord as a constructor dependency.class ThreadRepository ... protected function newThread(ThreadRecord $record) { return new Thread($record); } Pros: Hides the persistence record behind the domain interface. Easy to implement. Cons: The domain object is now dependent on the persistence layer, which is not the direction of dependencies we'd prefer. "},{"id":"\/cassini\/orm\/domain.html#2-1-11-5","title":"2.1.11.5. Map From Persistence To Domain","content":"Most difficult, but with the best separation, is to map the individual parts of the persistence record over to a \"plain old PHP object\" (POPO) in the domain, perhaps something like the following:namespace App\\Domain\\Thread; class Thread implements ThreadInterface { protected $id; protected $title; protected $body; protected $datePublished; protected $authorId; protected $authorName; protected $tags; protected $replies; public function __construct( $id, $title, $body, $datePublished, $authorId, $authorName, array $tags, array $replies ) { $this->id = $id; $this->title = $title; $this->body = $body; $this->datePublished = $datePublished; $this->authorId = $authorId; $this->authorName = $authorName; $this->tags = $tags; $this->replies = $replies; } public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->datePublished; } public function getAuthorId() { return $this->authorId; } public function getAuthorName() { return $this->authorName; } public function getTags() { return $this->tags; } public function getReplies() { return $this->replies; } } Now the ThreadRepository::newThread() factory method has a lot of work to do. It needs to map the individual fields in the persistence record to the domain object properties.class ThreadRepository ... protected function newThread(ThreadRecord $record) { $tags = []; foreach ($record->taggings as $tagging) { $tags[] = $tagging->tag->getArrayCopy(); } return new Thread( $record->thread_id, $record->title, $record->body, $record->date_published, $record->author->author_id, $record->author->name, $tags, $record->replies->getArrayCopy() ); } Pros: Offers true separation of domain from persistence. Cons: Tedious and time-consuming to implement. "},{"id":"\/cassini\/orm\/domain.html#2-1-11-6","title":"2.1.11.6. Which Approach Is Best?","content":"\"It depends.\" What does it depend on? How much time you have available, and what kind of suffering you are willing to put up with.If you need something quick, fast, and in a hurry, implementing the domain interface in the persistence layer will do the trick. However, it will come back to bite in you just as quickly, as you begin to realize that you need different domain behaviors in different contexts, all built from the same backing persistence records.If you are willing to deal with the trouble that comes from depending on the persistence layer records inside your domain, and the possibility that other developers will expose the underlying record in subtle ways, then composing the record into the domain may be your best bet.The most formally-correct approach is to map the record fields over to domain object properties. This level of separation makes testing and modification of application logic much easier in the long run, but it takes a lot of time, attention, and discipline."},{"id":"\/cassini\/skeleton\/usage.html#2-2-1","title":"2.2.1. Usage","content":"You can write your persistence model classes by hand, but it's going to be tedious to do so. Instead, use the .\/vendor\/bin\/atlas-skeleton command to read the table information from the database and generate them for you."},{"id":"\/cassini\/skeleton\/usage.html#2-2-1-1","title":"2.2.1.1. Configure Connection","content":"First, create a PHP file to return an array of configuration parameters for skeleton generation. Provide an array of PDO connection arguments, a string for the namespace prefix, and a directory to write the classes to:\/\/ \/path\/to\/skeleton-config.php return [ 'pdo' => [ 'mysql:dbname=testdb;host=localhost', 'username', 'password', ], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', ]; Tip: If you happen to have a generic config file for other purposes, you can nest the Atlas configuration values inside that array. For example: \/\/ \/path\/to\/settings.php return [ 'foo' => [ 'bar' => [ 'atlas' => [ 'pdo' => [ 'mysql:dbname=testdb;host=localhost', 'username', 'password', ], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', ], ], ], ]; "},{"id":"\/cassini\/skeleton\/usage.html#2-2-1-2","title":"2.2.1.2. Generate Classes","content":"You can then invoke the skeleton generator using that config file:php .\/vendor\/bin\/atlas-skeleton.php \/path\/to\/skeleton-config.php Tip: If you nested the Atlas keys inside the config file, pass the dot-separated names of the array elements leading to the Atlas configuration array as an argument immediately after the file path. For example, given the above array of ['foo']['bar']['atlas']: php .\/vendor\/bin\/atlas-skeleton.php \/path\/to\/settings.php foo.bar.atlas Doing so will read every table in the database and create one DataSource directory for each of them, each with several classes:App \u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 DataSource \u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 Thread \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 Thread.php # mapper \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadEvents.php # mapper-level events \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadFields.php # trait with property names \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadRecord.php # single record \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadRecordSet.php # record collection \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadRelationships.php # relationship definitions \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadRow.php # table row \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadSelect.php # mapper-level query object \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadTable.php # table defintion and interactions \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadTableEvents.php # table-level events \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadTableSelect.php # table-level query object Most of these classes will be empty, and are provided so you can extend their behavior if you wish. They also serve to assist IDEs with autocompletion of return typehints. Warning: If you run the skeleton generator more than once, the following classes will be OVERWRITTEN and you will lose any changes to them: {TYPE}Fields.php {TYPE}Row.php {TYPE}Table.php The remaining classes will remain untouched. "},{"id":"\/cassini\/skeleton\/usage.html#2-2-1-3","title":"2.2.1.3. Custom Transformations","content":"If you are unsatisfied with how the skeleton generator transforms table names to persistence model type names, you can instantiate the Transform class in the config file under the transform key, and pass an array of table-to-type names to override the default transformations:\/\/ \/path\/to\/skeleton-config.php return [ 'pdo' => [ 'mysql:dbname=testdb;host=localhost', 'username', 'password', ], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', 'transform' => new \\Atlas\\Cli\\Transform([ 'table_name' => 'TypeName', \/\/ use a value of null to skip the table entirely ]); ]; Alternatively, provide a callable (or callable instance) of your own:\/\/ \/path\/to\/skeleton-config.php return [ 'pdo' => [ 'mysql:dbname=testdb;host=localhost', 'username', 'password', ], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', 'transform' => function (string $table) : ?string { \/\/ return the $table name after transforming it into \/\/ a persistence model type name, or return null to \/\/ skip the table entirely }, ]; "},{"id":"\/cassini\/skeleton\/usage.html#2-2-1-4","title":"2.2.1.4. Custom Templates","content":"You can override the templates used by the skeleton generator and provide your own instead. This lets you customize the code generation; for example, to add your own common methods or to extend intercessory classes.First, take a look at the default templates in the Atlas.Cli resources\/templates\/ directory: Type.tpl TypeEvents.tpl TypeFields.tpl TypeRecord.tpl TypeRecordSet.tpl TypeRelationships.tpl TypeRow.tpl TypeSelect.tpl TypeTable.tpl TypeTableEvents.tpl TypeTableSelect.tpl For each persistence model type name, the word \"Type\" in the filename will be replaced with the type; .tpl will be replaced with .php. For example, a threads table will become a Thread type, so the resulting files will be Thread.php, ThreadEvents.php, and so on.To override a default template, create a custom template file of the same name in a directory of your own choosing. Then, in the skeleton config file, set a 'templates' key to that directory:\/\/ \/path\/to\/skeleton-config.php return [ 'pdo' => [ 'mysql:dbname=testdb;host=localhost', 'username', 'password', ], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', 'templates' => '\/path\/to\/custom-templates-dir' ]; When you run the skeleton command, it will look there first for each template, and then use the default template only if there is not a custom one available.The skeleton file will replace these tokens in the template file with these values: {NAMESPACE} => The namespace value from the config file. {TYPE} => The persistence model type. {DRIVER} => The database driver type. {NAME} => The table name. {COLUMN_NAMES} => An array of column names from the table. {COLUMN_DEFAULTS} => An add of column default values from the table. {AUTOINC_COLUMN} => The name of the autoincrement column, if any. {PRIMARY_KEY} => An array of primary key column names. {COLUMNS} => An array of the full column descriptions. {AUTOINC_SEQUENCE} => The name of the auotincrement sequence, if any. {PROPERTIES} => A partial docblock of properties for a Row. {FIELDS} => A partial docblock of field names for a Record. "},{"id":"\/cassini\/skeleton\/usage.html#2-2-1-5","title":"2.2.1.5. Use of PostgreSQL Schemas","content":"The use of a schema other than public in a PostgreSQL database can be done via a custom PDO instance. The following example sets the schema to custom_schema:$pdo = new PDO( 'pgsql:dbname=testdb;host=localhost', 'username', 'password', ); $pdo->exec('SET search_path TO custom_schema'); return [ 'pdo' => [$pdo], 'namespace' => 'App\\\\DataSource', 'directory' => '.\/src\/App\/DataSource', ]; "},{"id":"\/cassini\/table\/getting-started.html#2-3-1-1","title":"2.3.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/table. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/table\": \"~1.0\" } } "},{"id":"\/cassini\/table\/getting-started.html#2-3-1-2","title":"2.3.1.2. Instantiation","content":"Before using Atlas.Table, you will need to create the prerequsite data-source classes using the skeleton generator.Once you have your data source classes in place, create a TableLocator using the static new() method and pass your PDO connection parameters:use Atlas\\Table\\TableLocator; $tableLocator = TableLocator::new('sqlite::memory:'); Tip: Alternatively, you can pass an already-created Atlas.Pdo Connection object. You can then use the locator to retrieve a Table by its class name.use Atlas\\Testing\\DataSource\\Thread\\ThreadTable; $threadTable = $tableLocator->get(ThreadTable::CLASS); From there you can select, insert, update, and delete Row objects on a table, or work with table-level query objects."},{"id":"\/cassini\/table\/reading.html#2-3-2-1","title":"2.3.2.1. Fetching A Single Row","content":"Use the fetchRow() method to retrieve a single Row. It can be called either by primary key, or with a select() query.\/\/ fetch by primary key thread_id = 1 $threadRow = $threadTable->fetchRow('1'); $threadRow = $threadTable ->select() ->where('thread_id = ', '1') ->fetchRow(); Note: If fetchRow() does not find a match, it will return null. "},{"id":"\/cassini\/table\/reading.html#2-3-2-2","title":"2.3.2.2. Fetching An Array Of Rows","content":"The fetchRows() method works the same as fetchRow(), but returns an array of Rows. It can be called either with primary keys, or with a select() query.\/\/ fetch thread_id 1, 2, and 3 $threadRows = $threadTable->fetchRows([1, 2, 3]); \/\/ This is identical to the example above, but uses the `select()` variation. $threadRows = $threadTable ->select() ->where('thread_id IN ', [1, 2, 3]) ->fetchRows(); Note: If fetchRows() does not find a match, it will return an empty array. "},{"id":"\/cassini\/table\/reading.html#2-3-2-3","title":"2.3.2.3. Query Construction","content":"The Table::select() method returns a query object that you access to all the underlying SQL query methods. See the query system documentation for more information."},{"id":"\/cassini\/table\/reading.html#2-3-2-4","title":"2.3.2.4. Identifier Quoting","content":"The select() method will automatically quote the table name in the FROM clause.In addition, the fetchRow() and fetchRows() methods will automatically quote the primary key column names in the WHERE clause.You can manually quote any other identifiers using the Select object's quoteIdentifier() method."},{"id":"\/cassini\/table\/writing.html#2-3-3-1","title":"2.3.3.1. Creating and Inserting A Row","content":"Create a new Row using the newRow() method. You can assign data using properties, or pass an array of initial data to populate into the Row.$threadRow = $atlas->newRow([ 'title' => 'New Thread Title', ]); You can assign a value via a property, which maps to a column name.$date = new \\DateTime(); $threadRow->date_added = $date->format('Y-m-d H:i:s'); You can insert a single Row into the database by using the insertRow() method:$threadTable->insertRow($threadRow); Warning: The insertRow() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. Inserting a Row into a table with an auto-incrementing primary key will automatically modify the Row to set the last-inserted ID."},{"id":"\/cassini\/table\/writing.html#2-3-3-2","title":"2.3.3.2. Updating an Existing Row","content":"Updating an existing row works the same as insertRow().\/\/ fetch an existing row by primary key $threadRow = $threadTable->fetchRow(3); \/\/ modify the title $threadRow->title = 'This title is better than the last one'; \/\/ save the row back to the database $threadTable->updateRow($threadRow); Warning: The updateRow() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. Note: The updateRow() method will only send the row data changes back to the database, not the entire row. If there were no changes to the row data, calling updateRow() will be a no-op. "},{"id":"\/cassini\/table\/writing.html#2-3-3-3","title":"2.3.3.3. Deleting a Row","content":"Deleting a row works the same as inserting or updating.$threadRow = $threadTable->fetchRow(3); $threadTable->deleteRow($threadRow); Warning: The deleteRow() method will not catch exceptions; you may wish to wrap the method call in a try\/catch block. "},{"id":"\/cassini\/table\/writing.html#2-3-3-4","title":"2.3.3.4. Table-Wide Operations","content":"Whereas insertRow(), updateRow(), and deleteRow() operate on individual Row objects, you can perform table-wide operations using insert(), update(), and delete(). These latter three methods return Atlas.Query objects for you to work with as you see fit; call perform() on them directly to execute the query and get back a PDOStatement.See the Atlas.Query documentation for more information on the insert, update, and delete query objects."},{"id":"\/cassini\/table\/writing.html#2-3-3-5","title":"2.3.3.5. Identifier Quoting","content":"The insert(), update(), and delete() methods will automatically quote the table name in the INTO and FROM clauses.In addition, the Insert and Update query objects themselves will automatically quote the column names being inserted or updated.You can manually quote any other identifiers using the query object's quoteIdentifier() method."},{"id":"\/cassini\/table\/events.html#2-3-4","title":"2.3.4. Table Events","content":"Each generated Table class has its own corresponding TableEvents class. The TableEvents methods are invoked automatically at different points in the Table interactions:\/\/ runs after any Select query is created function modifySelect(Table $table, TableSelect $select) : void \/\/ runs after a newly-selected Row is populated function modifySelectedRow(Table $table, Row $row) : void \/\/ runs after any Insert query is created function modifyInsert(Table $table, Insert $insert) : void \/\/ runs after any Update query is created function modifyUpdate(Table $table, Update $update) : void \/\/ runs after any Delete query is created function modifyDelete(Table $table, Delete $delete) : void \/\/ runs before a Row-specific Insert query is created function beforeInsertRow(Table $table, Row $row) : ?array \/\/ runs after the Row-specific Insert query is created, but before it is performed function modifyInsertRow(Table $table, Row $row, Insert $insert) : void \/\/ runs after the Row-specific Insert query is performed function afterInsertRow( Table $table, Row $row, Insert $insert, PDOStatement $pdoStatement ) : void \/\/ runs before the Row-specific Update query is created function beforeUpdateRow(Table $table, Row $row) : ?array \/\/ runs after the Row-specific Update query is created, but before it is performed function modifyUpdateRow(Table $table, Row $row, Update $update) : void \/\/ runs after the Row-specific Update query is performed function afterUpdateRow( Table $table, Row $row, Update $update, PDOStatement $pdoStatement ) : void \/\/ runs before the Row-specific Delete query is created function beforeDeleteRow(Table $table, Row $row) : void \/\/ runs after the Row-specific Delete query is created, but before it is performed function modifyDeleteRow(Table $table, Row $row, Delete $delete) : void \/\/ runs after the Row-specific Delete query is performed function afterDeleteRow( Table $table, Row $row, Delete $delete, PDOStatement $pdoStatement ) : void Note: The methods beforeInsertRow() and beforeUpdateRow() optionally return an array. If they return an array, that array's key-value pairs will be used for the insert or update. Otherwise, the difference between the Row's previous values and its current values will be calculated, and their difference will be used for the insert or update. For example, when you call Table::updateRow(), these events run in this order: beforeUpdateRow() modifyUpdate() modifyUpdateRow() afterUpdateRow() Note that merely calling update() to get a table-wide Update query will only run the modifyUpdate() method, as it is not a row-specific interaction.TableEvents are the place to put behaviors such as setting inserted_at or updated_at values, etc:namespace Blog\\DataSource\\Posts; use Atlas\\Table\\Row; use Atlas\\Table\\Table; use Atlas\\Table\\TableEvents; class PostsTableEvents extends TableEvents { public function beforeInsertRow(Table $table, Row $row) { $row->inserted_at = date('Y-m-d H:i:s'); } public function beforeUpdateRow(Table $table, Row $row) { $row->updated_at = date('Y-m-d H:i:s'); } } "},{"id":"\/cassini\/query\/getting-started.html#2-4-1-1","title":"2.4.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/query. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/query\": \"~1.0\" } } "},{"id":"\/cassini\/query\/getting-started.html#2-4-1-2","title":"2.4.1.2. Instantiation","content":"Given an existing PDO instance, you can create a query using its static new() method:<?php use Atlas\\Query\\Select; use Atlas\\Query\\Insert; use Atlas\\Query\\Update; use Atlas\\Query\\Delete; $pdo = new PDO('sqlite::memory:'); $select = Select::new($pdo); $insert = Insert::new($pdo); $udpate = Update::new($pdo); $delete = Delete::new($pdo); (This also works with an existing Atlas.Pdo Connection instance.)Alternatively, instantiate a QueryFactory ...$queryFactory = new \\Atlas\\Query\\QueryFactory(); ...and then use the factory to create query objects for an Atlas.Pdo Connection.$connection = \\Atlas\\Pdo\\Connection::new('sqlite::memory:'); $select = $queryFactory->newSelect($connection); $insert = $queryFactory->newInsert($connection); $update = $queryFactory->newUpdate($connection); $delete = $queryFactory->newDelete($connection); "},{"id":"\/cassini\/query\/binding.html#2-4-2","title":"2.4.2. Value Binding","content":"Atlas.Query allows you to bind values to the SQL statement in various ways."},{"id":"\/cassini\/query\/binding.html#2-4-2-1","title":"2.4.2.1. Implicit Inline Binding","content":"Many Atlas.Query methods allow for inline binding of values. This means that the provided value will be represented by an auto-generated placeholder name in the query string, and the value itself will be retained for binding into that placeholder at query execution time.For example, given this query ...$select ->columns('*') ->from('foo') ->where('bar = ', $bar_value); \/\/ binds $bar_value inline ... a subsequent call to getStatement() will return:SELECT * FROM foo WHERE bar = :__1__ (The auto-generated placeholder name will increment each time an inline value gets bound.)If $bar_value is foo-bar, calling getBindValues() will return:[ ':__1__' => ['foo-bar', \\PDO::PARAM_STR], ] Note that the placeholder is automatically recognized as a string; the same will be true for nulls, integers, and floats.If you want to explicitly bind the value as some other type, you can pass that type after the value:$select ->columns('*') ->from('foo') ->where('bar = ', $bar_value, \\PDO::PARAM_LOB); If you bind an array inline, Atlas.Query will set a bind each element separately with its own placeholder, comma-separate the placeholders, and wrap them in parentheses. This makes using an IN() condition very convenient.$bar_value = ['foo', 'bar', 'baz']; \/\/ SELECT * FROM foo WHERE bar IN (:__1__, :__2__, :__3__) $select ->columns('*') ->from('foo') ->where('bar IN ', $bar_value); Finally, if the inline value is itself a Select object, it will be converted to a string via getStatement() and returned surrounded in parentheses:\/\/ SELECT * FROM foo WHERE bar IN (SELECT baz FROM dib) $select ->columns('*') ->from('foo') ->where('bar IN ', $select->subSelect() ->columns('baz') ->from('dib') ); "},{"id":"\/cassini\/query\/binding.html#2-4-2-2","title":"2.4.2.2. \nsprintf() Inline Binding","content":"If you need to bind more than one value into a condition, you can use an sprintf variation of implicit binding. Pass an expression string formatted for sprintf along with the values to bind:\/\/ SELECT * FROM foo WHERE bar BETWEEN :__1__ AND :__2__ $select ->columns('*') ->from('foo') ->whereSprintf( 'bar BETWEEN %s AND %s', $low_value, $high_value ); Note that you should use only %s in the format string, since it is the placeholder token that will be interpolated into the expression, not the actual value."},{"id":"\/cassini\/query\/binding.html#2-4-2-3","title":"2.4.2.3. Explicit Parameter Binding","content":"You can still use the normal PDO binding approach, where you explicitly set named parameters in conditions, and then bind the values with a separate call:$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValue('bar', $bar_value); ->bindValue('baz', $baz_value); These too will automatically recognize strings, nulls, integers, and floats, and set the approporate PDO parameter type. If you want to explicitly bind the value as some other type, pass an option third parameter to bindValue():$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValue('bar', $bar_value, \\PDO::PARAM_LOB); ->bindValue('baz', $baz_value); You can also bind multiple values at once ...$select ->columns('*') ->from('foo') ->where('bar = :bar') ->orWhere('baz = :baz') ->bindValues([ 'bar' => $bar_value, 'baz' => $baz_value ); ... but in that case you will not be able to explicitly set the parameter types.The automatic binding of array elements, as with implicit inline binding, does not work with explicit parameter binding."},{"id":"\/cassini\/query\/select.html#2-4-3-1-1","title":"2.4.3.1.1. Columns","content":"To add columns to the Select, use the columns() method and pass each column as a variadic argument.\/\/ SELECT id, name AS namecol, COUNT(foo) AS foo_count $select ->columns('id') ->columns('name AS namecol', 'COUNT(foo) AS foo_count'); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-2","title":"2.4.3.1.2. FROM","content":"To add a FROM clause, use the from() method:\/\/ FROM foo, bar AS b $select ->from('foo') ->from('bar AS b'); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-3","title":"2.4.3.1.3. JOIN","content":"(All JOIN methods support inline value binding via optional trailing arguments.)To add a JOIN clause, use the join() method:\/\/ LEFT JOIN doom AS d ON foo.id = d.foo_id $select->join( 'LEFT', 'doom AS d', 'foo.id = d.foo_id' ); You can concatenate onto the end of the most-recent JOIN using the catJoin() method:\/\/ LEFT JOIN doom AS d ON foo.id = d.foo_if AND d.bar = :__1__ AND d.baz = :__2__ $select ->join( 'LEFT', 'doom AS d', 'foo.id = d.foo_id AND d.bar = ', $bar_value )->catJoin(' AND d.baz = ', $baz_value); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-4","title":"2.4.3.1.4. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)To add WHERE conditions, use the where() method. Additional calls to where() will implicitly AND the subsequent condition.\/\/ WHERE bar > :__1__ AND zim >= :__2__ AND baz :__3__ $select ->where('bar > ', $bar_value) ->where('zim >= ', $zim_value) ->andWhere('baz < ', $baz_value); Use orWhere() to OR the subsequent condition.\/\/ WHERE bar > :__1__ OR zim >= :__2__ $select ->where('bar > ', $bar_value) ->orWhere('zim >= ', $zim_value) You can concatenate onto the end of the most-recent WHERE condition using the catWhere() method:\/\/ WHERE bar > :__1__ OR (foo = 88 AND bar < :__2__) $select ->where('bar > ', $bar_value) ->orWhere('(') ->catWhere('foo = 88') ->catWhere(' AND bar < ', $bar_value) ->catWhere(')'); Each of the WHERE-related methods has an sprintf variation as well:\/\/ WHERE bar BETWEEN :__1__ AND :__2__ \/\/ AND baz BETWEEN :__3__ AND :__4__ \/\/ OR dib BETWEEN :__5__ AND :__6___ \/\/ ... $select ->whereSprintf('bar BETWEEN %s AND %s', $bar_low, $bar_high) ->andWhereSprintf('baz BETWEEN %s AND %s', $baz_low, $baz_high) ->orWhereSprintf('dib BETWEEN %s AND %s', $dib_low, $dib_high) ->catWhereSprintf(...); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-4-1","title":"2.4.3.1.4.1. Convenience Equality","content":"There is an additional whereEquals() convenience method that adds a series of ANDed equality conditions for you based on an array of key-value pairs: Given an array value, the condition will be IN (). Given an empty array, the condition will be FALSE (which means the query will return no results). Given a null value, the condition will be IS NULL. For all other values, the condition will be =. If you pass a key without a value, that key will be used as a raw unescaped condition.For example:\/\/ WHERE foo IN (:__1__, :__2__, :__3__) \/\/ AND bar IS NULL \/\/ AND baz = :__4__ \/\/ AND zim = NOW() \/\/ AND FALSE $select->whereEquals([ 'foo' => ['a', 'b', 'c'], 'bar' => null, 'baz' => 'dib', 'zim = NOW()', 'gir' => [], ]); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-5","title":"2.4.3.1.5. GROUP BY","content":"To add GROUP BY expressions, use the groupBy() method and pass each expression as a variadic argument.\/\/ GROUP BY foo, bar, baz $select ->groupBy('foo') ->groupBy('bar', 'baz'); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-6","title":"2.4.3.1.6. HAVING","content":"(All HAVING methods support implicit and sprintf() inline value binding.)The HAVING methods work just like their equivalent WHERE methods: having() and andHaving() AND a HAVING condition orHaving() ORs a HAVING condition catHaving() concatenates onto the end of the most-recent HAVING condition havingSprintf() and andHavingSprintf() AND a HAVING condition with sprintf() orHavingSprintf() ORs a HAVING condition with sprintf() catHavingSprintf() concatenates onto the end of the most-recent HAVING condition with sprintf() "},{"id":"\/cassini\/query\/select.html#2-4-3-1-7","title":"2.4.3.1.7. ORDER BY","content":"To add ORDER BY expressions, use the orderBy() method and pass each expression as a variadic argument.\/\/ ORDER BY foo, bar, baz $select ->orderBy('foo') ->orderBy('bar', 'baz'); By default, results are ordered in ascending order (ASC). To sort in a different order, add the revelant keyword. For example, to sort in descending order:\/\/ ORDER BY foo DESC $select ->orderBy('foo DESC') "},{"id":"\/cassini\/query\/select.html#2-4-3-1-8","title":"2.4.3.1.8. LIMIT, OFFSET, and Paging","content":"To set a LIMIT and OFFSET, use the limit() and offset() methods.\/\/ LIMIT 10 OFFSET 40 $select ->limit(10) ->offset(40); Alternatively, you can limit by \"pages\" using the page() and perPage() methods:\/\/ LIMIT 10 OFFSET 40 $select ->page(5) ->perPage(10); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-9","title":"2.4.3.1.9. DISTINCT, FOR UPDATE, and Other Flags","content":"You can set DISTINCT and FOR UPDATE flags on the query like so:$select ->distinct() ->forUpdate(); Each of those methods take an option boolean parameter to enable (true) or disable (false) the flag.You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL HIGH_PRIORITY flag like so:\/\/ SELECT HIGH_PRIORITY * FROM foo $select ->columns('*') ->from('foo') ->setFlag('HIGH_PRIORITY'); "},{"id":"\/cassini\/query\/select.html#2-4-3-1-10","title":"2.4.3.1.10. UNION","content":"To UNION or UNION ALL the current Select with a followup query, call one the union*() methods:\/\/ SELECT id, name FROM foo \/\/ UNION \/\/ SELECT id, name FROM bar $select ->columns('id', 'name') ->from('foo') ->union() ->columns('id', 'name') ->from('bar'); \/\/ SELECT id, name FROM foo \/\/ UNION ALL \/\/ SELECT id, name FROM bar $select ->columns('id', 'name') ->from('foo') ->unionAll() ->columns('id', 'name') ->from('bar'); "},{"id":"\/cassini\/query\/select.html#2-4-3-2","title":"2.4.3.2. Resetting Query Elements","content":"The Select class comes with the following methods to \"reset\" various clauses a blank state. This can be useful when reusing the same query in different variations (e.g., to re-issue a query to get a COUNT(*) without a LIMIT, to find the total number of rows to be paginated over). reset() removes all clauses from the query. resetColumns() removes all the columns to be selected. resetFrom() removes the FROM clause, including all JOIN sub-clauses. resetWhere() removes all WHERE conditions. resetGroupBy() removes all GROUP BY expressions. resetHaving() removes all HAVING conditions. resetOrderBy() removes all ORDER BY expressions. resetLimit() removes all LIMIT, OFFSET, and paging values. resetFlags() removes all flags. Resetting only works on the current SELECT being built; it has no effect on queries that are already part of UNION."},{"id":"\/cassini\/query\/select.html#2-4-3-3","title":"2.4.3.3. Subselect Objects","content":"If you want create a subselect, call the subSelect() method:$subSelect = $select->subSelect(); The returned object will be a new Select that shares bound values with the parent Select.When you are done building the subselect, give it an alias using the as() method, and call getStatement() to render it into the desired condition or expression.The following is a contrived example:\/\/ SELECT * FROM ( \/\/ SELECT id, name \/\/ FROM foo \/\/ WHERE id > :__1__ \/\/ ) AS subfoo \/\/ WHERE LENGTH(subfoo.name) > :__2__ $select ->columns('*') ->from( $select->subSelect() ->columns('id', 'name') ->from('foo') ->where('id > ', $id) ->as('sub_alias') ->getStatement() ) ) ->where('LENGTH(sub_alias.name) > ', $length); The above shows how the bound values are shared between the parent and the sub Select objects. If you create a new Select and try to use it as a subselect, the bound values will not be shared, and you may get unexpected results.Other examples include:\/\/ joining on a subselect $select->join( 'LEFT', $select->subSelect()->...->as('sub_alias')->getStatement(), 'foo.id = sub_alias.id', ); \/\/ binding a subselect inline; note that it does not need to be \/\/ converted to a string via getStatement() $select->where( 'foo IN ', $select->subSelect()->... ); "},{"id":"\/cassini\/query\/select.html#2-4-3-4","title":"2.4.3.4. Performing The Query","content":"Once you have built the query, call the perform() method to execute it and get back a PDOStatement.$result = $select->perform(); The Select proxies all fetch*() and yield() method calls to the underlying Connection object via the magic __call() method, which means you can both build the query and perform it using the same Select object.The Connection fetch*() and yield*() methods proxied through the Select are as follows: fetchAll() : array fetchAffected() : int fetchColumn(int $column = 0) : array fetchGroup(int $style = PDO::FETCH_COLUMN) : array fetchKeyPair() : array fetchObject(string $class = 'stdClass', array $args = []) : object fetchObjects(string $class = 'stdClass', array $args = []) : array fetchOne() : ?array fetchUnique() : array fetchValue() : mixed yieldAll() : Generator yieldColumn(int $column = 0) : Generator yieldKeyPair() : Generator yieldObjects(string $class = 'stdClass', array $args = []) : Generator yieldUnique() : Generator For example, to build a query and get back an array of all results:\/\/ SELECT * FROM foo WHERE bar > :__1__ $result = $select ->columns('*') ->from('foo') ->where('bar > ', $value) ->fetchAll(); foreach ($result as $key => $val) { echo $val['bar'] . PHP_EOL; } For more information on the fetch*() and yield*() methods, please see the Atlas.Pdo Connection documentation."},{"id":"\/cassini\/query\/insert.html#2-4-4-1-1","title":"2.4.4.1.1. Into","content":"Use the into() method to specify the table to insert into.$insert->into('foo'); "},{"id":"\/cassini\/query\/insert.html#2-4-4-1-2","title":"2.4.4.1.2. Columns","content":"You can set a named placeholder and its corresponding bound value using the column() method.\/\/ INSERT INTO foo (bar) VALUES (:bar) $insert->column('bar', $bar_value); Note that the PDO parameter type will automatically be set for strings, integers, floats, and nulls. If you want to set a PDO parameter type yourself, pass it as an optional third parameter.\/\/ INSERT INTO foo (bar) VALUES (:bar); $insert->column('bar', $bar_value, \\PDO::PARAM_LOB); You can set several placeholders and their corresponding values all at once by using the columns() method:\/\/ INSERT INTO foo (bar) VALUES (:bar) $insert->columns([ 'bar' => $bar_value, 'baz' => $baz_value ]); However, you will not be able to specify a particular PDO parameter type when doing do.Bound values are automatically quoted and escaped; in some cases, this will be inappropriate, so you can use the raw() method to set column to an unquoted and unescaped expression.\/\/ INSERT INTO foo (bar) VALUES (NOW()) $insert->raw('bar', 'NOW()'); "},{"id":"\/cassini\/query\/insert.html#2-4-4-1-3","title":"2.4.4.1.3. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Insert using the returning() method, specifying columns as variadic arguments.\/\/ INSERT ... RETURNING foo, bar, baz $insert ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/cassini\/query\/insert.html#2-4-4-1-4","title":"2.4.4.1.4. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ INSERT LOW_PRIORITY INTO foo (bar) VALUES (:bar) $insert ->into('foo') ->column('bar', $bar_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/cassini\/query\/insert.html#2-4-4-2","title":"2.4.4.2. Performing The Query","content":"Once you have built the query, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $insert->perform(); "},{"id":"\/cassini\/query\/insert.html#2-4-4-2-1","title":"2.4.4.2.1. Last Insert ID","content":"If the database autoincrements a column while performing the query, you can get back that value using the getLastInsertId() method:$id = $insert->getLastInsertId(); Note: You can pass a sequence name as an optional parameter to getLastInsertId(); this may be required with PostgreSQL. "},{"id":"\/cassini\/query\/insert.html#2-4-4-2-2","title":"2.4.4.2.2. RETURNING","content":"If you added a RETURNING clause with the returning() method, you can retrieve those column values with the returned PDOStatement:$pdoStatement = $insert->perform(); $values = $pdoStatement->fetch(); \/\/ : array "},{"id":"\/cassini\/query\/update.html#2-4-5-1-1","title":"2.4.5.1.1. Table","content":"Use the table() method to specify the table to update.$update->table('foo'); "},{"id":"\/cassini\/query\/update.html#2-4-5-1-2","title":"2.4.5.1.2. Columns","content":"You can set a named placeholder and its corresponding bound value using the column() method.\/\/ UPDATE foo SET bar = :bar $update->column('bar', $bar_value); Note that the PDO parameter type will automatically be set for strings, integers, floats, and nulls. If you want to set a PDO parameter type yourself, pass it as an optional third parameter.\/\/ UPDATE foo SET bar = :bar $update->column('bar', $bar_value, \\PDO::PARAM_LOB); You can set several placeholders and their corresponding values all at once by using the columns() method:\/\/ UPDATE foo SET bar = :bar, baz = :baz $update->columns([ 'bar' => $bar_value, 'baz' => $baz_value ]); However, you will not be able to specify a particular PDO parameter type when doing do.Bound values are automatically quoted and escaped; in some cases, this will be inappropriate, so you can use the raw() method to set column to an unquoted and unescaped expression.\/\/ UPDATE foo SET bar = NOW() $update->raw('bar', 'NOW()'); "},{"id":"\/cassini\/query\/update.html#2-4-5-1-3","title":"2.4.5.1.3. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)The Update WHERE methods work just like their equivalent Select methods: where() and andWhere() AND a WHERE condition orWhere() ORs a WHERE condition catWhere() concatenates onto the end of the most-recent WHERE condition whereSprintf() and andWhereSprintf() AND a WHERE condition with sprintf() orWhereSprintf() ORs a WHERE condition with sprintf() catWhereSprintf() concatenates onto the end of the most-recent WHERE condition with sprintf() "},{"id":"\/cassini\/query\/update.html#2-4-5-1-4","title":"2.4.5.1.4. ORDER BY","content":"Some databases (notably MySQL) recognize an ORDER BY clause. You can add one to the Update with the orderBy() method; pass each expression as a variadic argument.\/\/ UPDATE ... ORDER BY foo, bar, baz $update ->orderBy('foo') ->orderBy('bar', 'baz'); "},{"id":"\/cassini\/query\/update.html#2-4-5-1-5","title":"2.4.5.1.5. LIMIT and OFFSET","content":"Some databases (notably MySQL and SQLite) recognize a LIMIT clause; others (notably SQLite) recognize an additional OFFSET. You can add these to the Update with the limit() and offset() methods:\/\/ LIMIT 10 OFFSET 40 $update ->limit(10) ->offset(40); "},{"id":"\/cassini\/query\/update.html#2-4-5-1-6","title":"2.4.5.1.6. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Update using the returning() method, specifying columns as variadic arguments.\/\/ UPDATE ... RETURNING foo, bar, baz $update ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/cassini\/query\/update.html#2-4-5-1-7","title":"2.4.5.1.7. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ UPDATE LOW_PRIORITY foo SET bar = :bar WHERE baz = :__1__ $update ->table('foo') ->column('bar', $bar_value) ->where('baz = ', $baz_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/cassini\/query\/update.html#2-4-5-2","title":"2.4.5.2. Performing The Query","content":"Once you have built the query, call the perform() method to execute it and get back a PDOStatement.$pdoStatement = $update->perform(); If you added a RETURNING clause with the returning() method, you can retrieve those column values with the returned PDOStatement:$pdoStatement = $update->perform(); $values = $pdoStatement->fetch(); \/\/ : array "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-1","title":"2.4.6.1.1. FROM","content":"Use the from() method to specify FROM expression.$delete->from('foo'); "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-2","title":"2.4.6.1.2. WHERE","content":"(All WHERE methods support implicit and sprintf() inline value binding.)The Delete WHERE methods work just like their equivalent Select methods: where() and andWhere() AND a WHERE condition orWhere() ORs a WHERE condition catWhere() concatenates onto the end of the most-recent WHERE condition whereSprintf() and andWhereSprintf() AND a WHERE condition with sprintf() orWhereSprintf() ORs a WHERE condition with sprintf() catWhereSprintf() concatenates onto the end of the most-recent WHERE condition with sprintf() "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-3","title":"2.4.6.1.3. ORDER BY","content":"Some databases (notably MySQL) recognize an ORDER BY clause. You can add one to the Delete with the orderBy() method; pass each expression as a variadic argument.\/\/ DELETE ... ORDER BY foo, bar, baz $delete ->orderBy('foo') ->orderBy('bar', 'baz'); "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-4","title":"2.4.6.1.4. LIMIT and OFFSET","content":"Some databases (notably MySQL and SQLite) recognize a LIMIT clause; others (notably SQLite) recognize an additional OFFSET. You can add these to the Delete with the limit() and offset() methods:\/\/ LIMIT 10 OFFSET 40 $delete ->limit(10) ->offset(40); "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-5","title":"2.4.6.1.5. RETURNING","content":"Some databases (notably PostgreSQL) recognize a RETURNING clause. You can add one to the Delete using the returning() method, specifying columns as variadic arguments.\/\/ DELETE ... RETURNING foo, bar, baz $delete ->returning('foo') ->returning('bar', 'baz'); "},{"id":"\/cassini\/query\/delete.html#2-4-6-1-6","title":"2.4.6.1.6. Flags","content":"You can set flags recognized by your database server using the setFlag() method. For example, you can set a MySQL LOW_PRIORITY flag like so:\/\/ DELETE LOW_PRIORITY foo WHERE baz = :__1__ $delete ->from('foo') ->where('baz = ', $baz_value) ->setFlag('LOW_PRIORITY'); "},{"id":"\/cassini\/query\/delete.html#2-4-6-2","title":"2.4.6.2. Performing The Query","content":"Once you have built the query, call the perform() method to execute it and get back a PDOStatement.$result = $delete->perform(); \/\/ : PDOStatement "},{"id":"\/cassini\/query\/other.html#2-4-7-1","title":"2.4.7.1. Microsoft SQL Server LIMIT and OFFSET","content":"If the Atlas.Pdo Connection is to a Microsoft SQL Server ('sqlsrv') instance, the LIMIT-related methods on the query object will generate sqlsrv-specific variations of LIMIT ... OFFSET: If only a LIMIT is present, it will be translated as a TOP clause. If both LIMIT and OFFSET are present, it will be translated as an OFFSET ... ROWS FETCH NEXT ... ROWS ONLY clause. In this case there must be an ORDER BY clause, as the offset clause is a sub-clause of ORDER BY. "},{"id":"\/cassini\/query\/other.html#2-4-7-2","title":"2.4.7.2. Identifier Quoting","content":"You can apply identifier quoting as needed by using the quoteIdentifier() method (available on all query objects).INSERT and UPDATE queries will automatically quote the column name that is being inserted or updated. No other automatic quoting of identifiers is applied."},{"id":"\/cassini\/query\/other.html#2-4-7-3","title":"2.4.7.3. Table Prefixes","content":"One frequently-requested feature for this package is support for \"automatic table prefixes\" on all queries. This feature sounds great in theory, but in practice it is (1) difficult to implement well, and (2) even when implemented it turns out to be not as great as it seems in theory. This assessment is the result of the hard trials of experience. For those of you who want modifiable table prefixes, we suggest using constants with your table names prefixed as desired; as the prefixes change, you can then change your constants."},{"id":"\/cassini\/info\/usage.html#2-5-1-1","title":"2.5.1.1. Instantiation","content":"Create an information-discovery object with a corresponding connection:use Atlas\\Info\\Info; use Atlas\\Pdo\\Connection; $connection = Connection::new('sqlite::memory:'); $info = Info::new($connection); Note: The new() method will automatically pick the right schema-discovery class for your Connection. "},{"id":"\/cassini\/info\/usage.html#2-5-1-2","title":"2.5.1.2. Fetching Table Names","content":"To get an array of table names from the current schema, call fetchTableNames().$tableNames = $info->fetchTableNames(); foreach ($tableNames as $tableName) { echo $tableName . PHP_EOL; } You can also indicate that you want table names from another schema:$tableNames = $info->fetchTableNames('schema_name'); "},{"id":"\/cassini\/info\/usage.html#2-5-1-3","title":"2.5.1.3. Fetching Column Defintions","content":"To get an array of column definitions in a table, call fetchColumns().$columns = $info->fetchColumns('table_name'); foreach ($columns as $name => $def) { echo \"Column $name is of type \" . $def['type'] . \" with a size of \" . $def['size'] . PHP_EOL; } Tip: You can use a schema prefix on the table name if you like, such as 'schema_name.table_name'. Each element is itself an array, with the following keys: name: (string) The column name type: (string) The column data type. Data types are as reported by the database. size: (?int) The column size (character length for strings, or numeric precision for numbers). scale: (?int) The number of decimal places for the column, if any. notnull: (bool) Is the column marked as NOT NULL? default: (mixed) The default value for the column. Note that this will be null if the underlying database uses an SQL expression instead of a literal string or number. autoinc: (bool) Is the column auto-incremented? primary: (bool) Is the column part of the primary key? "},{"id":"\/cassini\/info\/usage.html#2-5-1-4","title":"2.5.1.4. Fetching Auto-Increment Sequence Name","content":"To fetch the name of the primary key autoincrement sequence on a PostgreSQL table, call fetchAutoincSequence() with the table name.$sequence = $info->fetchAutoincSequence('table_name'); \/\/ => 'table_name_id_seq' On systems other than PostgreSQL, this will return null."},{"id":"\/cassini\/pdo\/connection.html#2-6-1-1","title":"2.6.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/pdo. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/pdo\": \"~1.0\" } } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-2","title":"2.6.1.2. Instantiation","content":"The easiest way to create a Connection is to use its static new() method, either with PDO connection arguments, or with an actual PDO instance:use Atlas\\Pdo\\Connection; \/\/ pass PDO constructor arguments ... $connection = Connection::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ ... or a PDO instance. $connection = Connection::new($pdo); If you need a callable factory to create a Connection and its PDO instance at a later time, such as in a service container, you can use the Connection::factory() method:use Atlas\\Pdo\\Connection; \/\/ get a callable factory that creates a Connection $factory = Connection::factory('sqlite::memory:'); \/\/ later, call the factory to instantiate the Connection $connection = $factory(); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-3","title":"2.6.1.3. Calling PDO Methods","content":"The Connection acts as a proxy to the decorated PDO instance, so you can call any method on the Connection that you would normally call on PDO."},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4","title":"2.6.1.4. Fetching Results","content":"The Connection provides several fetch*() methods to help reduce boilerplate code. Instead of issuing prepare(), a series of bindValue() calls, execute(), and then fetch*() on a PDOStatement, you can bind values and fetch results in one call on Connection directly."},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-1","title":"2.6.1.4.1. fetchAll()","content":"The plain-old PDO way to fetch all rows looks like this:use PDO; $pdo = new PDO('sqlite::memory:'); $stm = 'SELECT * FROM test WHERE foo = :foo AND bar = :bar'; $bind = ['foo' => 'baz', 'bar' => 'dib']; $sth = $pdo->prepare($stm); $sth->execute($bind); $result = $sth->fetchAll(PDO::FETCH_ASSOC); This is how to do the same thing with an Atlas PDO Connection:use Atlas\\Pdo\\Connection; $connection = Connection::new('sqlite::memory:'); $stm = 'SELECT * FROM test WHERE foo = :foo AND bar = :bar'; $bind = ['foo' => 'baz', 'bar' => 'dib']; $result = $connection->fetchAll($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-2","title":"2.6.1.4.2. fetchAffected()","content":"The fetchAffected() method returns the number of affected rows.$stm = \"UPDATE test SET incr = incr + 1 WHERE foo = :foo AND bar = :bar\"; $rowCount = $connection->fetchAffected($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-3","title":"2.6.1.4.3. fetchColumn()","content":"The fetchColumn() method returns a sequential array of the first column from all rows.$result = $connection->fetchColumn($stm, $bind); You can choose another column number with an optional third argument (columns are zero-indexed):\/\/ use column 3 (i.e. the 4th column) $result = $connection->fetchColumn($stm, $bind, 3); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-4","title":"2.6.1.4.4. fetchGroup()","content":"The fetchGroup() method is like fetchUnique() except that the values aren't wrapped in arrays. Instead, single column values are returned as a single dimensional array and multiple columns are returned as an array of arrays.$result = $connection->fetchGroup($stm, $bind, $style = PDO::FETCH_COLUMN) Set $style to PDO::FETCH_NAMED when values are an array (i.e. there are more than two columns in the select)."},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-5","title":"2.6.1.4.5. fetchKeyPair()","content":"The fetchKeyPair() method returns an associative array where each key is the first column and each value is the second column$result = $connection->fetchKeyPair($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-6","title":"2.6.1.4.6. fetchObject()","content":"The fetchObject() method returns the first row as an object of your choosing; the columns are mapped to object properties. an optional 4th parameter array provides constructor arguments when instantiating the object.$result = $connection->fetchObject($stm, $bind, 'ClassName', ['ctor_arg_1']); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-7","title":"2.6.1.4.7. fetchObjects()","content":"The fetchObjects() method returns an array of objects of your choosing; the columns are mapped to object properties. An optional 4th parameter array provides constructor arguments when instantiating the object.$result = $connection->fetchObjects($stm, $bind, 'ClassName', ['ctor_arg_1']); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-8","title":"2.6.1.4.8. fetchOne()","content":"The fetchOne() method returns the first row as an associative array where the keys are the column names.$result = $connection->fetchOne($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-9","title":"2.6.1.4.9. fetchUnique()","content":"The fetchUnique() method returns an associative array of all rows where the key is the value of the first column, and the row arrays are keyed on the remaining column names.$result = $connection->fetchUnique($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-4-10","title":"2.6.1.4.10. fetchValue()","content":"The fetchValue() method returns the value of the first row in the first column.$result = $connection->fetchValue($stm, $bind); "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5","title":"2.6.1.5. Yielding Results","content":"The Connection provides several yield*() methods to help reduce memory usage. Whereas fetch*() methods may collect all the query result rows before returning them all at once, the equivalent yield*() methods generate one result row at a time. For example:"},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5-1","title":"2.6.1.5.1. yieldAll()","content":"This is the yielding equivalent of fetchAll().foreach ($connection->yieldAll($stm, $bind) as $row) { \/\/ ... } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5-2","title":"2.6.1.5.2. yieldColumn()","content":"This is the yielding equivalent of fetchColumn(). foreach ($connection->yieldColumn($stm, $bind) as $val) { \/\/ ... } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5-3","title":"2.6.1.5.3. yieldKeyPair()","content":"This is the yielding equivalent of fetchKeyPair().foreach ($connection->yieldPairs($stm, $bind) as $key => $val) { \/\/ ... } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5-4","title":"2.6.1.5.4. yieldObjects()","content":"This is the yielding equivalent of fetchObjects().$class = 'ClassName'; $args = ['arg0', 'arg1', 'arg2']; foreach ($connection->yieldObjects($stm, $bind, $class, $args) as $object) { \/\/ ... } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-5-5","title":"2.6.1.5.5. yieldUnique()","content":"This is the yielding equivalent of fetchUnique().foreach ($connection->yieldUnique($stm, $bind) as $key => $row) { \/\/ ... } "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-6","title":"2.6.1.6. Query Logging","content":"It is sometimes useful to see a log of all queries passing through a Connection. To do so, call its logQueries() method, issue your queries, and then call getQueries().\/\/ start logging $connection->logQueries(true); \/\/ at this point, all query(), exec(), perform(), fetch*(), and yield*() \/\/ queries will be logged. \/\/ get the query log entries $queries = $connection->getQueries(); \/\/ stop logging $connection->logQueries(false); Each query log entry will be an array with these keys: start: when the query started finish: when the query finished duration: how long the query took performed: whether or not the query was actually perfomed; useful for seeing if a COMMIT actually occurred statement: the query statement string values: the array of bound values trace: an exception trace showing where the query was issued "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-6-1","title":"2.6.1.6.1. Logged Statements","content":"When queries are not being logged, Connection::prepare() will return a normal PDOStatement. However, when queries are being logged, Connection::prepare() will return an Atlas\\Pdo\\LoggedStatement instance instead.The LoggedStatement is an extension of PDOStatement, so it works the same way, but it has the added behavior of recording to the log when its execute() method is called."},{"id":"\/cassini\/pdo\/connection.html#2-6-1-6-2","title":"2.6.1.6.2. Custom Loggers","content":"You may wish to set a custom logger on the Connection. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.class CustomDebugger { public function __invoke(array $entry) : void { \/\/ call an injected logger to record the entry } } $customDebugger = new CustomDebugger(); $connection->setQueryLogger($customDebugger); $connection->logQueries(true); \/\/ now the Connection will send query log entries to the CustomDebugger Note: If you set a custom logger, the Connection will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Connection not show any new entries. "},{"id":"\/cassini\/pdo\/connection.html#2-6-1-7","title":"2.6.1.7. Persistent Connections","content":"Unlogged persistent Connection instances are fully supported, via the PDO::ATTR_PERSISTENT option at construction time.Logged persistent Connection instances are almost fully supported. The only exception to full support is that, on calling Connection::prepare(), the returned statement instance (PersistentLoggedStatement) does not honor the PDOStatement::bindColumn() method. All other methods and behaviors are fully supported."},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2","title":"2.6.2. Connection Locator","content":"Some applications may use multiple database servers; for example, one for writes, and one or more for reads. The ConnectionLocator allows you to define multiple Connection objects for lazy-loaded read and write connections. It will create the connections only when they are when called. The Connection creation logic should be wrapped in factory callable."},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-1","title":"2.6.2.1. Instantiation","content":"The easiest way to create a ConnectionLocator is to use its static new() method, either with PDO connection arguments, or with an actual PDO instance:use Atlas\\Pdo\\ConnectionLocator; \/\/ pass PDO constructor arguments ... $connectionLocator = ConnectionLocator::new( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); \/\/ ... or a PDO instance. $connectionLocator = ConnectionLocator::new($pdo); Doing so will define the default connection factory for the ConnectionLocator."},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-2","title":"2.6.2.2. Runtime Configuration","content":"Once you have a ConnectionLocator, you can add as many named read and write connection factories as you like:\/\/ the write (master) server $connectionLocator->setWriteFactory('master', Connection::factory( 'mysql:host=master.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #1 $connectionLocator->setReadFactory('slave1', Connection::factory( 'mysql:host=slave1.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #2 $connectionLocator->setReadFactory('slave2', Connection::factory( 'mysql:host=slave2.db.localhost;dbname=database', 'username', 'password' )); \/\/ read (slave) #3 $connectionLocator->setReadFactory('slave3', Connection::factory( 'mysql:host=slave3.db.localhost;dbname=database', 'username', 'password' )); "},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-3","title":"2.6.2.3. Getting Connections","content":"Retrieve a Connection from the locator when you need it. This will create the Connection (if needed) and then return it. getDefault() will return the default Connection. getRead() will return a random read Connection; after the first call, getRead() will always return the same Connection. (If no read Connections are defined, it will return the default connection.) getWrite() will return a random write Connection; after the first call, getWrite() will always return the same Connection. (If no write Connections are defined, it will return the default connection.) $read = $connectionLocator->getRead(); $results = $read->fetchAll('SELECT * FROM table_name LIMIT 10'); $readAgain = $connectionLocator->getRead(); assert($read === $readAgain); \/\/ true You can get any read or write connection directly by name using the get() method:$foo = $connectionLocator->get(ConnectionLocator::READ, 'foo'); $bar = $connectionLocator->get(ConnectionLocator::WRITE, 'bar'); "},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-4","title":"2.6.2.4. Locking To The Write Connection","content":"If you call the lockToWrite() method, calls to getRead() will return the write connection instead of the read connection.$read = $connectionLocator->getRead(); $write = $connectionLocator->getWrite(); $connectionLocator->lockToWrite(); $readAgain = $connectionLocator->getRead(); assert($readAgain === $write); \/\/ true You can disable the lock-to-write behavior by calling lockToWrite(false)."},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-5","title":"2.6.2.5. Construction-Time Configuration","content":"The ConnectionLocator can be configured with all its connections at construction time; this can be useful with dependency injection mechanisms. (Note that this requires using the constructor proper, not the static new() method.)use Atlas\\Pdo\\Connection; use Atlas\\Pdo\\ConnectionLocator; \/\/ default connection $default = Connection::factory( 'mysql:host=default.db.localhost;dbname=database', 'username', 'password' ); \/\/ read connections $read = [ 'slave1' => Connection::factory( 'mysql:host=slave1.db.localhost;dbname=database', 'username', 'password' ), 'slave2' => Connection::factory( 'mysql:host=slave2.db.localhost;dbname=database', 'username', 'password' ), 'slave3' => Connection::factory( 'mysql:host=slave3.db.localhost;dbname=database', 'username', 'password' ), ]; \/\/ write connection $write = [ 'master' => Connection::factory( 'mysql:host=master.db.localhost;dbname=database', 'username', 'password' ), ]; \/\/ configure locator at construction time $connectionLocator = new ConnectionLocator($default, $read, $write); "},{"id":"\/cassini\/pdo\/connection-locator.html#2-6-2-6","title":"2.6.2.6. Query Logging","content":"As with an individual Connection, it is sometimes useful to log all queries on all connections in the ConnectionLocator. To do so, call its logQueries() method, issue your queries, and then call getQueries() to get back the log entries.\/\/ start logging $connectionLocator->logQueries(true); \/\/ retrieve connections and issue queries, then: $queries = $connectionLocator->getQueries(); \/\/ stop logging $connectionLocator->logQueries(false); Each query log entry will have one added key, connection, indicating which connection performed the query. The connection label will be DEFAULT for the default connection, READ: and the read connection name, or WRITE: and the write connection name. Note: Calling logQueries() will turn logging on and off for all instances in the locator, even if those instances are not \"in hand\" at the moment. That is, you do not have to re-get the instance; logging for each connection will be turned on and off \"at a distance.\" You may wish to set a custom logger on the ConnectionLocator. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.class CustomDebugger { public function __invoke(array $entry) : void { \/\/ call an injected logger to record the entry } } $customDebugger = new CustomDebugger(); $connectionLocator->setQueryLogger($customDebugger); $connectionLocator->logQueries(true); \/\/ now the Connection will send query log entries to the CustomDebugger Note: If you set a custom logger, the Connection will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Connection not show any new entries. "},{"id":"\/boggs\/mapper\/getting-started.html#3-1-1-1","title":"3.1.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/orm. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/orm\": \"~2.0\" }, \"require-dev\": { \"atlas\/cli\": \"~1.0\" } } (The atlas\/cli package provides the atlas-skeleton command-line tool to help create skeleton classes for the mapper.)"},{"id":"\/boggs\/mapper\/getting-started.html#3-1-1-2","title":"3.1.1.2. Creating Data Source Classes","content":"You can create your data source classes by hand, but it's going to be tedious to do so. Instead, use the atlas-skeleton command to read the table information from the database. You can read more about that in the atlas\/cli docs."},{"id":"\/boggs\/mapper\/getting-started.html#3-1-1-3","title":"3.1.1.3. Instantiating Atlas","content":"Create an Atlas instance using the AtlasBuilder.The builder accepts a PDO, ExtendedPdo, or ConnectionLocator instance, or you can enter connection parameters and the builder will create a connection for you.<?php $atlasBuilder = new AtlasBuilder(new PDO(...)); \/\/ or $atlasBuilder = new AtlasBuilder(new ExtendedPdo(...)); \/\/ or $atlasBuilder = new AtlasBuilder(new ConnectionLocator(...)); \/\/ or $atlasBuilder = new AtlasBuilder( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); Then get a new Atlas instance out of the builder:<?php $atlas = $atlasBuilder->newAtlas(); Note: Atlas 2.5.x and earlier use the older AtlasContainer to build an Atlas object. The older approach requires you to register all mappers with the container using its setMappers() method, whereas the newer AtlasBuilder in 2.6.x lazy-loads them on demand. "},{"id":"\/boggs\/mapper\/relationships.html#3-1-2","title":"3.1.2. Mapper Relationships","content":"You can add relationships to a mapper inside its setRelated() method, calling one of the five available relationship-definition methods: oneToOne($field, $mapperClass) (aka \"has one\") manyToOne($field, $mapperClass) (aka \"belongs to\") oneToMany($field, $mapperClass) (aka \"has many\") manyToMany($field, $mapperClass, $throughField) (aka \"has many through\") manyToOneByReference($field, $referenceCol) (aka \"polymorphic association\") The $field will become a field name on the returned Record object. That field will be populated from the specified $mapperClass in Atlas. (In the case of manyToMany(), the association mappings will come from the specified $throughField.)Here is an example:<?php namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\AuthorMapper; use App\\DataSource\\Summary\\SummaryMapper; use App\\DataSource\\Reply\\ReplyMapper; use App\\DataSource\\Tagging\\TaggingMapper; use App\\DataSource\\Tag\\TagMapper; use Atlas\\Orm\\Mapper\\AbstractMapper; class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS); $this->oneToOne('summary', SummaryMapper::CLASS); $this->oneToMany('replies', ReplyMapper::CLASS); $this->oneToMany('taggings', TaggingMapper::CLASS); $this->manyToMany('tags', TagMapper::CLASS, 'taggings'); } } "},{"id":"\/boggs\/mapper\/relationships.html#3-1-2-1","title":"3.1.2.1. Relationship Key Columns","content":"By default, in all relationships except many-to-one, the relationship will take the primary key column(s) in the native table, and map to those same column names in the foreign table.In the case of many-to-one, it is the reverse; that is, the relationship will take the primary key column(s) in the foreign table, and map to those same column names in the native table.If you want to use different columns, call the on() method on the relationship. For example, if the threads table uses author_id, but the authors table uses just id, you can do this:<?php class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS) ->on([ \/\/ native (threads) column => foreign (authors) column 'author_id' => 'id', ]); \/\/ ... } } And on the oneToMany side of the relationship, you use the native author table id column with the foreign threads table author_id column.<?php class AuthorMapper extends AbstractMapper { protected function setRelated() { $this->oneToMany('threads', ThreadMapper::CLASS) ->on([ \/\/ native (author) column => foreign (threads) column 'id' => 'author_id', ]); \/\/ ... } } "},{"id":"\/boggs\/mapper\/relationships.html#3-1-2-2","title":"3.1.2.2. Composite Relationship Keys","content":"Likewise, if a table uses a composite key, you can re-map the relationship on multiple columns. If table foo has composite primary key columns of acol and bcol, and it maps to table bar on foo_acol and foo_bcol, you would do this:<?php class FooMapper { protected function setRelated() { $this->oneToMany('bars', BarMapper::CLASS) ->on([ \/\/ native (foo) column => foreign (bar) column 'acol' => 'foo_acol', 'bcol' => 'foo_bcol', ]); } } "},{"id":"\/boggs\/mapper\/relationships.html#3-1-2-3","title":"3.1.2.3. Case-Sensitivity","content":" Note: This applies only to string-based relationship keys. If you are using numeric relationship keys, this section does not apply. Atlas will match records related by string keys in a case-senstive manner. If your collations on the related string key columns are not case sensitive, Atlas might not match up related records properly in memory after fetching them from the database. This is because 'foo' and 'FOO' might be equivalent in the database collation, but they are not equivalent in PHP.In that kind of situation, you will want to tell the relationship to ignore the case of related string key columns when matching related records. You can do so with the ignoreCase() method on the relationship definition. <?php class FooMapper { protected function setRelated() { $this->oneToMany('bars', BarMapper::CLASS) ->ignoreCase(); } } With that in place, a native value of 'foo' match to a foreign value of 'FOO' when Atlas is stitching together related records."},{"id":"\/boggs\/mapper\/relationships.html#3-1-2-4","title":"3.1.2.4. Simple WHERE Conditions","content":"You may find it useful to define simple WHERE conditions on the foreign side of the relationship. For example, you can handle one side of a many-to-one relationship by reference (aka \"polymorphic association\") by selecting only related records of a particular type.In the following example, a comments table has a commentable_id column as the foreign key value, but is restricted to \"video\" values on a discriminator column named commentable_type.class IssueMapper extends AbstractMapper { protected function setRelated() { $this->oneToMany('comments', CommentMapper::CLASS) ->on([ 'video_id' => 'commentable_id' ]) ->where('commentable_type = ?', 'video'); } } (These conditions will be honored by MapperSelect::*joinWith() as well.)"},{"id":"\/boggs\/mapper\/relationships.html#3-1-2-5","title":"3.1.2.5. Relationships By Reference","content":"The many-to-one relationship by reference is somewhat different from the other relationship types. It is identical to a many-to-one relationship, except that the relationships vary by a reference column in the native table. This allows rows in the native table to \"belong to\" rows in more than one foreign table. The typical example is one of comments that can be created on many different kinds of content, such as static pages, blog posts, and video links.class CommentMapper extends AbstractMapper { protected function setRelated() { \/\/ The first argument is the field name on the native record; \/\/ the second argument is the reference column on the native table. $this->manyToOneByReference('commentable', 'commentable_type') \/\/ The first argument is the value of the commentable_type column; \/\/ the second is the related foreign mapper class; \/\/ the third is the native-to-foreign column mapping. ->to('page', PageMapper::CLASS, ['commentable_id' => 'page_id']) ->to('post', PostMapper::CLASS, ['commentable_id' => 'post_id']) ->to('video', VideoMapper::CLASS, ['commentable_id' => 'video_id']); } } Note that there will be one query per type of reference in the native record set. That is, if a native record set (of an arbitrary number of records) refers to a total of three different relationships, then Atlas will issue three additional queries to fetch the related records.(The phrase \"relationship by reference\" is used here instead of \"polymorphic association\" because the latter is an OOP term, not an SQL term. The former is more SQL-ish.)"},{"id":"\/boggs\/mapper\/reading.html#3-1-3","title":"3.1.3. Fetching Records and RecordSets","content":"Use Atlas to retrieve a single Record, an array of Records, or a collection of Records in a RecordSet, from the database."},{"id":"\/boggs\/mapper\/reading.html#3-1-3-1","title":"3.1.3.1. Fetching a Record","content":"Use the fetchRecord() method to retrieve a single Record. It can be called either by primary key, or with a select() query.<?php \/\/ fetch by primary key thread_id = 1 $threadRecord = $atlas->fetchRecord( ThreadMapper::class, '1' ); $threadRecord = $atlas ->select(ThreadMapper::class) ->where('thread_id = ?', '1') ->fetchRecord(); Tip: The select() variation gives you access to all the underlying SQL query methods. See Aura\\SqlQuery for more information. Note: If fetchRecord() does not find a match, it will return null. Warning: If using the select() variation with the cols() method, be sure to include the table's primary key column(s) if you are fetching a Record. If using one of the other fetch*() methods outlined in the chapter on Direct Queries, then this isn't necessary. See below. <?php \/\/ must include the primary key column (and author_id because of the \/\/ where clause) $threadRecord = $atlas ->select(ThreadMapper::class) ->where('author_id = ?', '2') ->cols(['thread_id', 'title', 'author_id']) ->fetchRecord(); \/\/ No need to include the primary key column $threadRecord = $atlas ->select(ThreadMapper::class) ->where('author_id = ?', '2') ->cols(['title', 'author_id']) ->fetchOne(); "},{"id":"\/boggs\/mapper\/reading.html#3-1-3-1-1","title":"3.1.3.1.1. Accessing\/Reading Record Data","content":"Once you have a Record, you can access the columns via properties on the Record. Assume a database column called title.<?php echo $thread->title; See also the page on working with Records."},{"id":"\/boggs\/mapper\/reading.html#3-1-3-2","title":"3.1.3.2. Fetching An Array Of Records","content":"The fetchRecords() method works the same as fetchRecord(), but returns an array of Records. It can be called either with primary keys, or with a select() query.<?php \/\/ fetch thread_id 1, 2, and 3 $threadRecordSet = $atlas->fetchRecords( ThreadMapper::CLASS, [1, 2, 3] ); \/\/ This is identical to the example above, but uses the `select()` variation. $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->where('thread_id IN (?)', [1, 2, 3]) ->fetchRecords(); To return all rows, use the select() variation as shown below.<?php \/\/ Use the `select()` variation to fetch all records, optionally ordering the \/\/ returned results $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['date_added DESC']) ->fetchRecords(); Tip: The select() variation gives you access to all the underlying SQL query methods. See Aura\\SqlQuery for more information. "},{"id":"\/boggs\/mapper\/reading.html#3-1-3-3","title":"3.1.3.3. Fetching A RecordSet Collection","content":"The fetchRecordSet() method works just the same as fetchRecords(), but instead of returning an array of Records, it returns a RecordSet collection. Note: If fetchRecordSet() does not find any matches, it will return an empty RecordSet collection object. To check if the RecordSet contains any Records, call the isEmpty() method on the RecordSet. "},{"id":"\/boggs\/mapper\/reading.html#3-1-3-3-1","title":"3.1.3.3.1. Accessing\/Reading RecordSet Data","content":"RecordSets act as arrays of Records. As such, you can easily iterate over the RecordSet and access the Records individually.<?php \/\/ fetch the top 100 threads $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['thread_id DESC']) ->limit(100) ->fetchRecordSet(); foreach ($threadRecordSet as $threadRecord) { echo $threadRecord->title; } See also the page on working with RecordSets."},{"id":"\/boggs\/mapper\/reading.html#3-1-3-4","title":"3.1.3.4. Fetching Related Records","content":"Any relationships that are set in the Mapper will appear as null in the Record object. Related data will only be populated if it is explicitly requested as part of the fetch or select.On a fetch*(), load relateds using a third argument: an array specifying which related fields to retrieve.<?php $threadRecord = $atlas->fetchRecord( ThreadMapper::CLASS, '1', [ 'author', 'summary', 'replies', ] ); $threadRecordSet = $atlas->fetchRecordSet( ThreadMapper::CLASS, [1, 2, 3], [ 'author', 'summary', 'replies', ] ); When using the select() variation, load relateds using the with() method:<?php $threadRecord = $atlas ->select(ThreadMapper::class) ->where('thread_id = ?', '1') ->with([ 'author', 'summary', 'replies', ]) ->fetchRecord(); $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->where('thread_id IN (?)', [1, 2, 3]) ->with([ 'author', 'summary', 'replies', ]) ->fetchRecordSet(); Note: When fetching a manyToMany relationship, you must explicitly specify both the association (through) related AND the manyToMany related. Additionally, you must specify these relationships in the correct order. <?php $threadRecord = $atlas->fetchRecord(ThreadMapper::CLASS, '1', [ 'taggings', \/\/ specify the \"through\" first ... 'tags' \/\/ ... then the manyToMany ]); Relationships can be nested as deeply as needed. For example, to fetch the author of each reply on each thread:<?php $threadRecord = $this->atlas ->select(ThreadMapper::class) ->where('thread_id = ?', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author' ] ]) ->fetchRecord(); Alternatively, you can pass a closure to exercise fine control over the query that fetches the relateds:<?php \/\/ fetch thread_id 1; with only the last 10 related replies in descending order; \/\/ including each reply author $threadRecord = $atlas->fetchRecord(ThreadMapper::CLASS, '1', [ 'author', 'summary', 'replies' => function ($selectReplies) { $selectReplies ->limit(10) ->orderBy(['reply_id DESC']) ->with([ 'author' ]); }, ]); "},{"id":"\/boggs\/mapper\/reading.html#3-1-3-4-1","title":"3.1.3.4.1. Accessing\/Reading Related Data","content":"Accessing related data works just like accessing Record properties except instead of using a column name, you use the relationship name defined in the mapper.<?php $threadRecord = $this->atlas ->select(ThreadMapper::class) ->where('thread_id = ?', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author' ] ]) ->fetchRecord(); \/\/ Assume the author table has a column named `last_name` foreach ($threadRecord->replies as $reply) { echo $reply->author->last_name; } If you specify with() on a one-to-one or many-to-one relationship that returns no result, the related field will be populated with false. If you specify with() on a one-to-many or many-to-many relationship that returns no result, the field will be populated with an empty RecordSet collection."},{"id":"\/boggs\/mapper\/reading.html#3-1-3-5","title":"3.1.3.5. Returning Data in Other Formats","content":"You can return a Record or a RecordSet as an array rather than a Record or RecordSet object using the getArrayCopy() method.<?php $threadRecord = $atlas->fetchRecord('ThreadMapper::CLASS', '1'); $threadArray = $threadRecord->getArrayCopy(); $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['date_added DESC']) ->fetchRecordSet(); $threadsArray = $threadRecordSet->getArrayCopy(); JSON-encoding Records and RecordSets is trival.<?php $threadJson = json_encode($threadRecord); $threadsJson = json_encode($threadRecordSet); "},{"id":"\/boggs\/mapper\/reading.html#3-1-3-6","title":"3.1.3.6. Reading Record Counts","content":"If you use a select() to fetch a RecordSet with a limit() or page(), you can re-use the select to get a count of how many Records would have been returned. This can be useful for paging displays.<?php $select = $atlas ->select(ThreadMapper::CLASS) ->with([ 'author', 'summary', 'replies' ]) ->limit(10) ->offset(20); $threadRecordSet = $select->fetchRecordSet(); $countOfAllThreads = $select->fetchCount(); "},{"id":"\/boggs\/mapper\/records.html#3-1-4-1","title":"3.1.4.1. Creating and Inserting a Record","content":"Create a new Record using the newRecord() method. You can assign data using properties, or pass an array of initial data to populate into the Record.<?php $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', ] ); You can assign a value via a property, which maps to a column name.<?php $date = new \\DateTime(); $thread->date_added = $date->format('Y-m-d H:i:s'); You can insert a single Record back to the database by using the Atlas::insert() method. This will use the appropriate Mapper for the Record to perform the write within a transaction, and capture any exceptions that occur along the way.<?php $success = $atlas->insert($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } Inserting a Record with an auto-incrementing primary key will automatically modify the Record to set the last-inserted ID.Inserting a Record will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.In the following example, assume a Thread Record has a manyToOne relationship with an Author Record using the author_id column. The relationship is named author. (See the section on relationships for more information.)<?php $author = $atlas->fetchRecord(AuthorMapper::CLASS, 4); $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', 'author'=>$author ] ); \/\/ If the insert is successful, the `author_id` column will automatically be \/\/ set to the Author Record's primary key value. In this case, 4. $success = $atlas->insert($thread); echo $thread->author_id; \/\/ 4 Note: If the Author Record is new, Atlas will NOT automatically insert the new Author and set the foreign key on the new Author Record via the insert() method. This can, however, be achieved using the persist() method. This is discussed later in this chapter. The following will fail.<?php $author = $atlas->newRecord(AuthorMapper::CLASS, [ 'first_name'=>'Sterling', 'last_name'=>'Archer' ] ); $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', 'author'=>$author ] ); \/\/ Insert will not create the related Author Record. Use persist() instead. $success = $atlas->insert($thread); "},{"id":"\/boggs\/mapper\/records.html#3-1-4-2","title":"3.1.4.2. Updating an Existing Record","content":"Updating an existing record works the same as insert().<?php \/\/ fetch an existing record by primary key $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); \/\/ Modify the title $thread->title = 'This title is better than the last one'; \/\/ Save the record back to the database. $success = $atlas->update($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } As with insert(), foreign keys are also updated, but only for existing related records.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); $author = $atlas->fetchRecord(AuthorMapper::CLASS, 4); \/\/ Modify the author $thread->author = $author; \/\/ Save the record back to the database. $success = $atlas->update($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } "},{"id":"\/boggs\/mapper\/records.html#3-1-4-3","title":"3.1.4.3. Deleting a Record","content":"Deleting a record works the same as inserting or updating.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); $success = $atlas->delete($thread); if ($success) { echo \"Removed record from the database.\"; } else { echo \"Did not remove the Record: \" . $atlas->getException(); } "},{"id":"\/boggs\/mapper\/records.html#3-1-4-4","title":"3.1.4.4. Persisting a Record","content":"If you like, you can persist a Record and all of its loaded relationships (and all of their loaded relationships, etc.) back to the database using the Atlas persist() method. This is good for straightforward relationship structures where the order of write operations does not need to be closely managed.The persist() method will: persist many-to-one and many-to-many relateds loaded on the native Record; persist the native Record by ... inserting the Row for the Record if it is new; or, updating the Row for the Record if it has been modified; or, deleting the Row for the Record if the Record has been marked for deletion using the Record::markForDeletion() method; persist one-to-one and one-to-many relateds loaded on the native Record. <?php $success = $atlas->persist($record); if ($success) { echo \"Wrote the Record and all of its relateds back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } As with insert and update, this will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.If a related field is not loaded, it cannot be persisted automatically.Note that whether or not the Row for the Record is inserted\/updated\/deleted, the persist() method will still recursively traverse all the related fields and persist them as well.The delete() method will not attempt to cascade deletion or nullification across relateds at the ORM level. Your database may have cascading set up at the database level; Atlas has no control over this."},{"id":"\/boggs\/mapper\/records.html#3-1-4-5","title":"3.1.4.5. Marking Records for Deletion","content":"You may also mark records for deletion and they will be removed from the database after a transaction is completed via persist().<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); \/\/ Mark the record for deletion $thread->markForDeletion(); $atlas->persist($thread); You can also mark several related Records for deletion and when the native Record is persisted, they will be deleted from the database.<?php \/\/ Assume a oneToMany relationship between a thread and its comments \/\/ Select the thread and related comments $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3, [ 'comments' ] ); \/\/ Mark each related comment for deletion foreach ($thread->comments as $comment) { $comment->markForDeletion(); } \/\/ Persist the thread and the comments are also deleted $atlas->persist($thread); "},{"id":"\/boggs\/mapper\/record-sets.html#3-1-5-1","title":"3.1.5.1. New RecordSets","content":"Create a new RecordSet using the newRecordSet() method.<?php $threadRecordSet = $atlas->newRecordSet(ThreadMapper::CLASS); "},{"id":"\/boggs\/mapper\/record-sets.html#3-1-5-2","title":"3.1.5.2. Appending Records to a RecordSet","content":"You can append a new Record to an existing RecordSet using appendNew(), optionally passing any data you want to initially populate into the Record:<?php $newThread = $threadRecordSet->appendNew([ 'title' => 'New Title', ]); Additionally, you can append foreign Records to a native Record's relateds.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 1, [ 'comments', ]); $comment = $thread->comments->appendNew([ 'thread_id' => $thread->thread_id, 'comment' => 'Lorem ipsum dolor sit amet...' ]); \/\/ plan to insert the new comment $transaction->insert($comment); \/\/ Or persist the thread $atlas->persist($thread); "},{"id":"\/boggs\/mapper\/record-sets.html#3-1-5-3","title":"3.1.5.3. Array Access","content":"The RecordSet also acts as an array, so you can get\/set\/unset Records by their sequential keys in the RecordSet.<?php \/\/ address the second record in the set $threadRecordSet[1]->title = 'Changed Title'; \/\/ unset the first record in the set unset($threadRecordSet[0]); \/\/ push a new record onto the set $threadRecordSet[] = $atlas->newRecord(ThreadMapper::CLASS); "},{"id":"\/boggs\/mapper\/record-sets.html#3-1-5-4","title":"3.1.5.4. Searching within RecordSets","content":"You can search for Records within an existing RecordSet by their column values:<?php $threadRecordSet = $atlas->select(ThreadMapper::CLASS) ->where('published=?', 1) ->fetchRecordSet(); \/\/ returns one matching Record object from the RecordSet, \/\/ or null if there is no match $matchingRecord = $threadRecordSet->getOneBy(['subject' => 'Subject One']); \/\/ returns an array of matching Record objects from the RecordSet $matchingRecords = $threadRecordSet->getAllBy(['author_id' => '5']); "},{"id":"\/boggs\/mapper\/record-sets.html#3-1-5-5","title":"3.1.5.5. Removing Records from RecordSets","content":"You can remove Records from a RecordSet by their column values. This does NOT delete Records from the database; it only removes them from the RecordSet.<?php \/\/ unsets and returns one matching Record from the Record Set, \/\/ or null if there is no match $removedRecord = $threadRecordSet->removeOneBy(['subject' => 'Subject One']); \/\/ unsets and returns an array of matching Record objects from the RecordSet $removedRecords = $threadRecordSet->removeAllBy(['author_id' => '5']); \/\/ unsets and returns an array of all Record object from the RecordSet $removedRecords = $threadRecordSet->removeAll(); Note: This only removes them from the RecordSet; it does not delete them from the database. If you need to delete a record from the database, see the sections on Marking Records for Deletion and deleting Records. "},{"id":"\/boggs\/mapper\/transactions.html#3-1-6","title":"3.1.6. Transactions (Unit of Work)","content":"If you make changes to several Records, you can write them back to the database using a unit-of-work Transaction. You can plan for Records to be inserted, updated, and deleted, in whatever order you like, and then execute the entire transaction plan at once. Exceptions will cause a rollback.<?php \/\/ create a transaction $transaction = $atlas->newTransaction(); \/\/ plan work for the transaction $transaction->insert($record1); $transaction->update($record2); $transaction->delete($record3); \/\/ or persist an entire record and its relateds $transaction->persist($record4); \/\/ execute the transaction plan $success = $transaction->exec(); if ($success) { echo \"The Transaction succeeded!\"; } else { \/\/ get the exception that was thrown in the transaction $e = $transaction->getException(); \/\/ get the work element that threw the exception $work = $transaction->getFailure(); \/\/ some output echo \"The Transaction failed: \"; echo $work->getLabel() . ' threw ' . $e->getMessage(); } "},{"id":"\/boggs\/mapper\/behavior.html#3-1-7","title":"3.1.7. Record and RecordSet Behaviors","content":"Atlas makes it easy to add your own behaviors to both Records and RecordSets. To accomplish this, you need a Record class for custom Record logic, and a RecordSet class for custom RecordSet logic. The Atlas CLI script (installable via composer using atlas\/cli), can create these classes for you, saving you from manually writing them.Consult the Atlas CLI documentation.It's important to note that the Record and RecordSet objects described below should only be used for very simple behaviors. Any non-trivial domain work may be an indication that you need a domain layer. See the documentation on Domain Models for examples of how you can use Atlas to build a domain layer.Here is an example using the atlas\/cli package and the --full option..\/vendor\/bin\/atlas-skeleton.php \\ --conn=\/path\/to\/conn.php \\ --dir=src\/App\/DataSource \\ --table=threads \\ --full \\ App\\\\DataSource\\\\Thread Upon completion, you will have a folder layout as follows:-- src -- App -- DataSource -- Thread -- ThreadMapper.php -- ThreadMapperEvents.php -- ThreadRecord.php -- ThreadRecordSet.php -- ThreadTable.php -- ThreadTableEvents.php Once you have a Record Class (ThreadRecord.php), you can create custom methods to call from your Record object.<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\Record; \/** * @inheritdoc *\/ class ThreadRecord extends Record { \/\/ Format the date_created property public function formatDate($format = 'M jS, Y') { $dateTime = new \\DateTime($this->date_created); return $dateTime->format($format); } } $thread = $atlas->fetchRecord(ThreadMapper::CLASS, $id); echo $thread->formatDate(); \/\/ outputs something like `Aug 21st, 2017` The same concept is available for RecordSets using the RecordSet class. In our example ThreadRecordSet.php.<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\RecordSet; \/** * @inheritdoc *\/ class ThreadRecordSet extends RecordSet { public function foo() { $data = [] foreach ($this as $record) { $data[] = $record->title; } return implode('; ', $data); } } $threads = $atlas->fetchRecordSet(ThreadMapper::CLASS, [1, 2, 3]); echo $threads->foo(); "},{"id":"\/boggs\/mapper\/events.html#3-1-8-1","title":"3.1.8.1. Mapper Events","content":"There are several events that will automatically be called when interacting with a Mapper object. If you used the Atlas CLI tool with the --full option, a MapperEvents class will be created for you. For example, ThreadMapperEvents.php. With this class, you can override any of the available mapper events.The insert(), update(), and delete() methods all have 3 events associated with them: a before*(), a modify*(), and an after*(). In addition, there is a modifySelect() event.<?php \/\/ Runs after the Select object is created, but before it is executed modifySelect(MapperInterface $mapper, MapperSelect $select) \/\/ Runs before the Insert object is created beforeInsert(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Insert object is created, but before it is executed modifyInsert(MapperInterface $mapper, RecordInterface $record, Insert $insert) \/\/ Runs after the Insert object is executed afterInsert(MapperInterface $mapper, RecordInterface $record, Insert $insert, PDOStatement $pdoStatement) \/\/ Runs before the Update object is created beforeUpdate(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Update object is created, but before it is executed modifyUpdate(MapperInterface $mapper, RecordInterface $record, Update $update) \/\/ Runs after the Update object is executed afterUpdate(MapperInterface $mapper, RecordInterface $record, Update $update, PDOStatement $pdoStatement) \/\/ Runs before the Delete object is created beforeDelete(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Delete object is created, but before it is executed modifyDelete(MapperInterface $mapper, RecordInterface $record, Delete $delete) \/\/ Runs after the Delete object is executed afterDelete(MapperInterface $mapper, RecordInterface $record, Delete $delete, PDOStatement $pdoStatement) Here is a simple example with the assumption that the Record object has a validate() method and a getErrors() method. See the section on Adding Logic to Records and RecordSets.<?php namespace Blog\\DataSource\\Posts; use Atlas\\Orm\\Mapper\\MapperEvents; use Atlas\\Orm\\Mapper\\MapperInterface; use Atlas\\Orm\\Mapper\\RecordInterface; use Atlas\\Orm\\Exception; \/** * @inheritdoc *\/ class PostsMapperEvents extends MapperEvents { public function beforeUpdate(MapperInterface $mapper, RecordInterface $record) { if (! $record->validate()) throw new Exception('Update Error'); } } } And you might have something like this in your code:<?php $success = $atlas->update($post); if ($sucess) { echo \"Post updated\"; } else { foreach ($post->getErrors as $error) { echo $error . '<br\/>'; } } "},{"id":"\/boggs\/mapper\/events.html#3-1-8-2","title":"3.1.8.2. Table Events","content":"There are several events that will automatically be called when interacting with a Table object. If you used the Atlas CLI tool with the --full option, a TableEvents class will be created for you. For example, ThreadTableEvents.php. With this class, you can override any of the available mapper events.The insert(), update(), and delete() methods all have 3 events associated with them: a before*(), a modify*(), and an after*(). In addition, there is a modifySelect() event, and a modifySelectedRow() event.<?php \/\/ Runs after the Select object is created, but before it is executed modifySelect(TableInterface $table, TableSelect $select) \/\/ Runs after a newly-selected row is instantiated, but before it is \/\/ identity-mapped. modifySelectedRow(TableInterface $table, Row $row) \/\/ Runs before the Insert object is created beforeInsert(TableInterface $table, Row $row) \/\/ Runs after the Insert object is created, but before it is executed modifyInsert(TableInterface $table, Row $row, Insert $insert) \/\/ Runs after the Insert object is executed afterInsert(TableInterface $table, Row $row, Insert $insert, PDOStatement $pdoStatement) \/\/ Runs before the Update object is created beforeUpdate(TableInterface $table, Row $row) \/\/ Runs after the Update object is created, but before it is executed modifyUpdate(TableInterface $table, Row $row, Update $update) \/\/ Runs after the Update object is executed afterUpdate(TableInterface $table, Row $row, Update $update, PDOStatement $pdoStatement) \/\/ Runs before the Delete object is created beforeDelete(TableInterface $table, Row $row) \/\/ Runs after the Delete object is created, but before it is executed modifyDelete(TableInterface $table, Row $row, Delete $delete) \/\/ Runs after the Delete object is executed afterDelete(TableInterface $table, Row $row, Delete $delete, PDOStatement $pdoStatement) If the beforeInsert() method returns an array, that array will be used for the insert data; otherwise, Row::getArrayCopy() will be used.If the beforeUpdate() method returns an array, that array will be used for the update data; otherwise, Row::getArrayDiff() will be used."},{"id":"\/boggs\/mapper\/direct.html#3-1-9","title":"3.1.9. Direct Queries","content":"If you need to perform queries directly, additional fetch* and yield* methods are provided which expose the Extended PDO functionality. By using the $cols parameter, you can select specific columns or individual values. For example:<?php \/\/ an array of IDs $threadIds = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id']) ->limit(10) ->orderBy('thread_id DESC') ->fetchCol(); \/\/ key-value pairs of IDs and titles $threadIdsAndTitles = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'tite']) ->limit(10) ->orderBy('thread_id DESC') ->fetchPairs(); \/\/ etc. See the list of ExtendedPdo::fetch*() and yield*() methods for more.You can also call fetchRow() or fetchRows() to get Row objects directly from the Table underlying the Mapper.Finally, in addition the various JOIN methods provided by Aura.SqlQuery, the MapperSelect also provides joinWith(), so that you can join on a defined relationship. (The related table will be aliased as the relationship name.) For example, to do an INNER JOIN with another table as defined in the Mapper relationships:<?php $threadIdsAndAuthorNames = $atlas ->select(ThreadMapper::CLASS) ->joinWith('INNER', 'author') ->cols([ \"thread.thread_id\", \"CONCAT(author.first_name, ' ', author.last_name)\" ]) ->limit(10) ->orderBy('thread_id DESC') ->fetchPairs(); "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-1","title":"3.1.9.1.1. Fetch Value","content":"Returns a single value, or null.<?php $subject = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->where('thread_id = ?', '1') ->fetchValue(); \/\/ \"Subject One\" "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-2","title":"3.1.9.1.2. Fetch Column","content":"Returns a sequential array of one column, or an empty array.<?php $subjects = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->limit(2) ->fetchCol(); \/\/ [ \/\/ 0 => \"Subject One\", \/\/ 1 => \"Subject Two\" \/\/ ] "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-3","title":"3.1.9.1.3. Fetch Pairs","content":"Returns an associative array where the key is the first column and the value is the second column, or an empty array.<?php $subjectAndBody = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->fetchPairs(); \/\/ [ \/\/ 'Subject One' => \"Body Text One\", \/\/ 'Subject Two' => \"Body Text Two\" \/\/ ] "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-4","title":"3.1.9.1.4. Fetch One","content":"Returns an associative array of one row, or null.<?php $threadData = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body', 'author_id']) ->where('thread_id = 1') ->fetchOne(); \/\/ [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\", \/\/ 'author_id' => \"1\" \/\/ ] "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-5","title":"3.1.9.1.5. Fetch Assoc","content":"Returns an associative array of rows keyed on the first column specified, or an empty array.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->fetchAssoc(); \/\/ [ \/\/ 'Subject One' => [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\", \/\/ ], \/\/ 'Subject Two' => [ \/\/ 'subject' => \"Subject Two\", \/\/ 'body' => \"Body Text Two\" \/\/ ] \/\/ ] "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-1-6","title":"3.1.9.1.6. Fetch All","content":"Returns a sequential array of associative arrays, or an empty array.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->orderBy('thread_id DESC') ->fetchAll(); \/\/ [ \/\/ 0 => [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\" \/\/ ], \/\/ 1 => [ \/\/ 'subject' => \"Subject Two\", \/\/ 'body' => \"Body Text Two\" \/\/ ] \/\/ ] "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-2","title":"3.1.9.2. Yielding Data","content":"If you prefer to get the results one at a time, you can use the yield* variations on these methods to iterate through the result set instead of returning an array."},{"id":"\/boggs\/mapper\/direct.html#3-1-9-2-1","title":"3.1.9.2.1. Yield Col","content":"Iterate through a sequential array of one column.<?php $subjects = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->yieldCol(); foreach($subjects as $subject) { echo $subject; } "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-2-2","title":"3.1.9.2.2. Yield Pairs","content":"Iterate through an associative array by the first column specified.<?php $subjectAndBody = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->yieldPairs(); foreach($subjectAndBody as $subject => $body) { echo $subject . \": \" . $body; } "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-2-3","title":"3.1.9.2.3. Yield Assoc","content":"Iterate through an associative array of rows by the first column specified.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'subject']) ->yieldAssoc(); foreach($threads as $threadId => $thread) { echo $threadId . \": \" . $thread['subject']; } "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-2-4","title":"3.1.9.2.4. Yield All","content":"Iterate through a sequential array of rows.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'subject']) ->yieldAll(); foreach($threads as $thread) { echo $thread['thread_id'] . \": \" . $thread['subject']; } "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-3","title":"3.1.9.3. Complex Queries","content":"You can use any of the direct table access methods with more complex queries and joins.<?php $threadData = $atlas ->select(ThreadMapper::CLASS) ->cols(['threads.subject', 'authors.name', 's.*']) ->join('INNER', 'authors', 'authors.author_id = threads.author_id') ->join('INNER', 'summary s', 's.thread_id = threads.thread_id') ->where('authors.name = ?', $name) ->orderBy('threads.thread_id DESC') ->offset(2) ->limit(2) ->fetchAssoc(); "},{"id":"\/boggs\/mapper\/direct.html#3-1-9-4","title":"3.1.9.4. Reusing the Select","content":"The select object can be used for multiple queries, which may be useful for pagination. The generated select statement can also be displayed for debugging purposes.<?php $select = $atlas ->select(ThreadMapper::CLASS) ->cols(['*']) ->offset(10) ->limit(5); \/\/ Fetch the current result set $results = $select->fetchAll(); \/\/ Fetch the row count without any limit or offset $totalCount = $select->fetchCount(); \/\/ View the generated select statement $statement = $select->getStatement(); "},{"id":"\/boggs\/mapper\/domain.html#3-1-10","title":"3.1.10. Domain Models","content":"You can go a long way with just your persistence model Records. However, at some point you may want to separate your persistence model Records from your domain model Entities and Aggregates. This section offers some suggestions and examples on how to do that."},{"id":"\/boggs\/mapper\/domain.html#3-1-10-1","title":"3.1.10.1. Persistence Model","content":"For the examples below, we will work with an imaginary forum application that has conversation threads. The ThreadMapper might something like this:<?php namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\AuthorMapper; use App\\DataSource\\Summary\\SummaryMapper; use App\\DataSource\\Reply\\ReplyMapper; use App\\DataSource\\Tagging\\TaggingMapper; use App\\DataSource\\Tag\\TagMapper; use Atlas\\Orm\\Mapper\\AbstractMapper; class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS); $this->oneToOne('summary', SummaryMapper::CLASS); $this->oneToMany('replies', ReplyMapper::CLASS); $this->oneToMany('taggings', TaggingMapper::CLASS); $this->manyToMany('tags', TagMapper::CLASS, 'taggings'); } } (We will leave the other mappers and their record classes for the imagination.)"},{"id":"\/boggs\/mapper\/domain.html#3-1-10-2","title":"3.1.10.2. Domain Model Interfaces","content":"At some point, we have decided we want to depend on domain Entities or Aggregates, rather than persistence Records, in our application.For example, the interface we want to use for a Thread Entity in domain might look like this:<?php namespace App\\Domain\\Thread; interface ThreadInterface { public function getId(); public function getSubject(); public function getBody(); public function getDatePublished(); public function getAuthorId(); public function getAuthorName(); public function getTags(); public function getReplies(); } (This interface allows us to typehint the application against these domain- specific Entity methods, rather than using the persistence Record properties.)Further, we will presume a naive domain repository implementation that returns Thread Entities. It might look something like this:<?php namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadMapper; class ThreadRepository { protected $mapper; public function __construct(ThreadMapper $mapper) { $this->mapper = $mapper; } public function fetchThread($thread_id) { $record = $this->mapper->fetchRecord($thread_id, [ 'author', 'taggings', 'tags', 'replies', ]); return $this->newThread($record); } protected function newThread(ThreadRecord $record) { \/* ??? *\/ } } The problem now is the newThread() factory method. How do we convert a persistence layer ThreadRecord into a domain layer ThreadInterface implementation?There are three options, each with different tradeoffs: Implement the domain interface in the persistence layer. Compose the persistence record into the domain object. Map the persistence record fields to domain implementation fields. "},{"id":"\/boggs\/mapper\/domain.html#3-1-10-3","title":"3.1.10.3. Implement Domain In Persistence","content":"The easiest thing to do is to implement the domain ThreadInterface in the persistence ThreadRecord, like so:<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\Record; use App\\Domain\\Thread\\ThreadInterface; class ThreadRecord extends Record implements ThreadInterface { public function getId() { return $this->thread_id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->date_published; } public function getAuthorId() { return $this->author->author_id; } public function getAuthorName() { return $this->author->name; } public function getTags() { return $this->tags->getArrayCopy(); } public function getReplies() { return $this->replies->getArrayCopy(); } } With this, the ThreadRepository::newThread() factory method doesn't actually need to factory anything at all. It just returns the persistence record, since the record now has the domain interface.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return $record; } Pros: Trivial to implement. Cons: Exposes the persistence layer Record methods and properties to the domain layer, where they can be easily abused. "},{"id":"\/boggs\/mapper\/domain.html#3-1-10-4","title":"3.1.10.4. Compose Persistence Into Domain","content":"Almost as easy, but with better separation, is to have a domain layer object that implements the domain interface, but encapsulates the persistence record as the data source. The domain object might look something like this:<?php namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadRecord; class Thread implements ThreadInterface { protected $record; public function __construct(ThreadRecord $record) { $this->record = $record; } public function getId() { return $this->record->thread_id; } public function getTitle() { return $this->record->title; } public function getBody() { return $this->record->body; } public function getDatePublished() { return $this->record->date_published; } public function getAuthorId() { return $this->record->author->author_id; } public function getAuthorName() { return $this->record->author->name; } public function getTags() { return $this->record->tags->getArrayCopy(); } public function getReplies() { return $this->record->replies->getArrayCopy(); } } Now the ThreadRepository::newThread() factory method has to do a little work, but not much. All it needs is to create the Thread domain object with the ThreadRecord as a constructor dependency.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return new Thread($record); } Pros: Hides the persistence record behind the domain interface. Easy to implement. Cons: The domain object is now dependent on the persistence layer, which is not the direction of dependencies we'd prefer. "},{"id":"\/boggs\/mapper\/domain.html#3-1-10-5","title":"3.1.10.5. Map From Persistence To Domain","content":"Most difficult, but with the best separation, is to map the individual parts of the persistence record over to a \"plain old PHP object\" (POPO) in the domain, perhaps something like the following:<?php namespace App\\Domain\\Thread; class Thread implements ThreadInterface { protected $id; protected $title; protected $body; protected $datePublished; protected $authorId; protected $authorName; protected $tags; protected $replies; public function __construct( $id, $title, $body, $datePublished, $authorId, $authorName, array $tags, array $replies ) { $this->id = $id; $this->title = $title; $this->body = $body; $this->datePublished = $datePublished; $this->authorId = $authorId; $this->authorName = $authorName; $this->tags = $tags; $this->replies = $replies; } public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->datePublished; } public function getAuthorId() { return $this->authorId; } public function getAuthorName() { return $this->authorName; } public function getTags() { return $this->tags; } public function getReplies() { return $this->replies; } } Now the ThreadRepository::newThread() factory method has a lot of work to do. It needs to map the individual fields in the persistence record to the domain object properties.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return new Thread( $record->thread_id, $record->title, $record->body, $record->date_published, $record->author->author_id, $record->author->name, $record->tags->getArrayCopy(), $record->replies->getArrayCopy() ); } Pros: Offers true separation of domain from persistence. Cons: Tedious and time-consuming to implement. "},{"id":"\/boggs\/mapper\/domain.html#3-1-10-6","title":"3.1.10.6. Which Approach Is Best?","content":"\"It depends.\" What does it depend on? How much time you have available, and what kind of suffering you are willing to put up with.If you need something quick, fast, and in a hurry, implementing the domain interface in the persistence layer will do the trick. However, it will come back to bite in you just as quickly, as you begin to realize that you need different domain behaviors in different contexts, all built from the same backing persistence records.If you are willing to deal with the trouble that comes from depending on the persistence layer records inside your domain, and the possibility that other developers will expose the underlying record in subtle ways, then composing the record into the domain may be your best bet.The most formally-correct approach is to map the record fields over to domain object properties. This level of separation makes testing and modification of application logic much easier in the long run, but it takes a lot of time, attention, and discipline."},{"id":"\/boggs\/skeleton\/getting-started.html#3-2-1","title":"3.2.1. Atlas.Cli 1.x","content":"This is the command-line interface package for Atlas. It is intended for use in your development environments, not your production ones."},{"id":"\/boggs\/skeleton\/getting-started.html#3-2-1-1","title":"3.2.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/cli.Add it to the require-dev section of your root-level composer.json to install the atlas-skeleton command-line tool.{ \"require-dev\": { \"atlas\/cli\": \"~1.0\" } } "},{"id":"\/boggs\/skeleton\/getting-started.html#3-2-1-2","title":"3.2.1.2. Creating Skeleton Classes","content":"You can create your data source classes by hand, but it's going to be tedious to do so. Instead, use the atlas-skeleton command to read the table information from the database.Create a PHP file to return an array of connection parameters suitable for PDO:<?php \/\/ \/path\/to\/conn.php return [ 'mysql:dbname=testdb;host=localhost', 'username', 'password' ]; ?> You can then invoke the skeleton generator using that connection. Specify a target directory for the skeleton files, and pass the namespace name for the data source classes. You can pass an explicit table name to keep the generator from trying to guess the name..\/vendor\/bin\/atlas-skeleton.php \\ --conn=\/path\/to\/conn.php \\ --dir=src\/App\/DataSource \\ --table=threads \\ App\\\\DataSource\\\\Thread N.b.: The backslashes (\\) at the end of the lines are to allow the command to be split across multiple lines in Unix. If you are on Windows, omit the trailing backslashes and enter the command on a single line. That will create this directory and two classes in src\/App\/DataSource\/:\u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 Thread \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadMapper.php \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadTable.php The Mapper class will be empty, and the Table class will a description of the specified --table. Note that you should not make changes to the table class, as they will be overwritten if you regenerate the skeleton.Do that once for each SQL table in your database.If you pass --full to atlas-skeleton, it will additionally generate empty MapperEvents, Record, RecordSet, and TableEvents classes. (These are useful only if you want to add custom behaviors, and will not be overwritten if you regenerate the skeleton.)The --full option will also add a Fields class with @property annotations for table columns on the Record. This file will be overwritten if the table class is regenerated, as it is defined from the table columns (which may have changed since the last skeleton generation)."},{"id":"\/albers\/mapper\/getting-started.html#4-1-1-1","title":"4.1.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/orm. Add the following lines to your composer.json file, then call composer update.{ \"require\": { \"atlas\/orm\": \"~1.0\" }, \"require-dev\": { \"atlas\/cli\": \"~1.0\" } } (The atlas\/cli package provides the atlas-skeleton command-line tool to help create skeleton classes for the mapper.)"},{"id":"\/albers\/mapper\/getting-started.html#4-1-1-2","title":"4.1.1.2. Creating Data Source Classes","content":"You can create your data source classes by hand, but it's going to be tedious to do so. Instead, use the atlas-skeleton command to read the table information from the database. You can read more about that in the atlas\/cli docs."},{"id":"\/albers\/mapper\/getting-started.html#4-1-1-3","title":"4.1.1.3. Instantiating Atlas","content":"Create an Atlas instance using the AtlasContainer.The container accepts a PDO, ExtendedPdo or ConnectionLocator instance or you can enter connection parameters and the container creates a connection for you.<?php $atlasContainer = new AtlasContainer(new PDO(...)); \/\/ or $atlasContainer = new AtlasContainer(new ExtendedPdo(...)); \/\/ or $atlasContainer = new AtlasContainer(new ConnectionLocator(...)); \/\/ or $atlasContainer = new AtlasContainer( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); Next, set the available mapper classes into the container.<?php $atlasContainer->setMappers([ AuthorMapper::CLASS, ReplyMapper::CLASS, SummaryMapper::CLASS, TagMapper::CLASS, ThreadMapper::CLASS, TaggingMapper::CLASS, ]); Finally, get back the Atlas instance out of the container.<?php $atlas = $atlasContainer->getAtlas(); "},{"id":"\/albers\/mapper\/relationships.html#4-1-2","title":"4.1.2. Mapper Relationships","content":"You can add relationships to a mapper inside its setRelated() method, calling one of the four available relationship-definition methods: oneToOne($field, $mapperClass) (aka \"has one\") manyToOne($field, $mapperClass) (aka \"belongs to\") oneToMany($field, $mapperClass) (aka \"has many\") manyToMany($field, $mapperClass, $throughField) (aka \"has many through\") The $field will become a field name on the returned Record object. That field will be populated from the specified $mapperClass in Atlas. (In the case of manyToMany(), the association mappings will come from the specified $throughField.)Here is an example:<?php namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\AuthorMapper; use App\\DataSource\\Summary\\SummaryMapper; use App\\DataSource\\Reply\\ReplyMapper; use App\\DataSource\\Tagging\\TaggingMapper; use App\\DataSource\\Tag\\TagMapper; use Atlas\\Orm\\Mapper\\AbstractMapper; class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS); $this->oneToOne('summary', SummaryMapper::CLASS); $this->oneToMany('replies', ReplyMapper::CLASS); $this->oneToMany('taggings', TaggingMapper::CLASS); $this->manyToMany('tags', TagMapper::CLASS, 'taggings'); } } "},{"id":"\/albers\/mapper\/relationships.html#4-1-2-1","title":"4.1.2.1. Relationship Key Columns","content":"By default, in all relationships except many-to-one, the relationship will take the primary key column(s) in the native table, and map to those same column names in the foreign table.In the case of many-to-one, it is the reverse; that is, the relationship will take the primary key column(s) in the foreign table, and map to those same column names in the native table.If you want to use different columns, call the on() method on the relationship. For example, if the threads table uses author_id, but the authors table uses just id, you can do this:<?php class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS) ->on([ \/\/ native (threads) column => foreign (authors) column 'author_id' => 'id', ]); \/\/ ... } } And on the oneToMany side of the relationship, you use the native author table id column with the foreign threads table author_id column.<?php class AuthorMapper extends AbstractMapper { protected function setRelated() { $this->oneToMany('threads', ThreadMapper::CLASS) ->on([ \/\/ native (author) column => foreign (threads) column 'id' => 'author_id', ]); \/\/ ... } } "},{"id":"\/albers\/mapper\/relationships.html#4-1-2-2","title":"4.1.2.2. Composite Relationship Keys","content":"Likewise, if a table uses a composite key, you can re-map the relationship on multiple columns. If table foo has composite primary key columns of acol and bcol, and it maps to table bar on foo_acol and foo_bcol, you would do this:<?php class FooMapper { protected function setRelated() { $this->oneToMany('bars', BarMapper::CLASS) ->on([ \/\/ native (foo) column => foreign (bar) column 'acol' => 'foo_acol', 'bcol' => 'foo_bcol', ]); } } "},{"id":"\/albers\/mapper\/relationships.html#4-1-2-3","title":"4.1.2.3. Case-Sensitivity","content":" Note: This applies only to string-based relationship keys. If you are using numeric relationship keys, this section does not apply. Atlas will match records related by string keys in a case-senstive manner. If your collations on the related string key columns are not case sensitive, Atlas might not match up related records properly in memory after fetching them from the database. This is because 'foo' and 'FOO' might be equivalent in the database collation, but they are not equivalent in PHP.In that kind of situation, you will want to tell the relationship to ignore the case of related string key columns when matching related records. You can do so with the ignoreCase() method on the relationship definition. <?php class FooMapper { protected function setRelated() { $this->oneToMany('bars', BarMapper::CLASS) ->ignoreCase(); } } With that in place, a native value of 'foo' match to a foreign value of 'FOO' when Atlas is stitching together related records."},{"id":"\/albers\/mapper\/relationships.html#4-1-2-4","title":"4.1.2.4. Simple WHERE Conditions","content":"You may find it useful to define simple WHERE conditions on the foreign side of the relationship. For example, you can handle one side of a so-called polymorphic relationship by selecting only related records of a particular type.In the following example, a comments table has a commentable_id column as the foreign key value, but is restricted to \"issue\" values on a discriminator column named commentable.class IssueMapper extends AbstractMapper { protected function setRelated() { $this->oneToMany('comments', CommentMapper::CLASS) ->on([ 'issue_id' => 'commentable_id' ]) ->where('commentable = ?', 'issue'); } } (These conditions will be honored by MapperSelect::*joinWith() as well.)"},{"id":"\/albers\/mapper\/reading.html#4-1-3","title":"4.1.3. Fetching Records and RecordSets","content":"Use Atlas to retrieve a single Record, or many Records in a RecordSet, from the database."},{"id":"\/albers\/mapper\/reading.html#4-1-3-1","title":"4.1.3.1. Fetching a Record","content":"Use the fetchRecord() method to retrieve a single Record. It can be called either by primary key, or with a select() query.<?php \/\/ fetch by primary key thread_id = 1 $threadRecord = $atlas->fetchRecord( ThreadMapper::class, '1' ); $threadRecord = $atlas ->select(ThreadMapper::class) ->where('thread_id = ?', '1') ->fetchRecord(); Tip: The select() variation gives you access to all the underlying SQL query methods. See Aura\\SqlQuery for more information. Note: If fetchRecord() does not find a match, it will return false. Warning: If using the select() variation with the cols() method, be sure to include the table's primary key column(s) if you are fetching a Record. If using one of the other fetch*() methods outlined in the chapter on Direct Queries, then this isn't necessary. See below. <?php \/\/ must include the primary key column (and author_id because of the \/\/ where clause) $threadRecord = $atlas ->select(ThreadMapper::class) ->where('author_id = ?', '2') ->cols(['thread_id', 'title', 'author_id']) ->fetchRecord(); \/\/ No need to include the primary key column $threadRecord = $atlas ->select(ThreadMapper::class) ->where('author_id = ?', '2') ->cols(['title', 'author_id']) ->fetchOne(); "},{"id":"\/albers\/mapper\/reading.html#4-1-3-1-1","title":"4.1.3.1.1. Accessing\/Reading Record Data","content":"Once you have a Record, you can access the columns via properties on the Record. Assume a database column called title.<?php echo $thread->title; "},{"id":"\/albers\/mapper\/reading.html#4-1-3-2","title":"4.1.3.2. Fetching A RecordSet","content":"The fetchRecordSet() method works the same as fetchRecord(), but for multiple Records. It can be called either with primary keys, or with a select() query.<?php \/\/ fetch thread_id 1, 2, and 3 $threadRecordSet = $atlas->fetchRecordSet( ThreadMapper::CLASS, [1, 2, 3] ); \/\/ This is identical to the example above, but uses the `select()` variation. $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->where('thread_id IN (?)', [1, 2, 3]) ->fetchRecordSet(); To return all rows, use the select() variation as shown below.<?php \/\/ Use the `select()` variation to fetch all records, optionally ordering the \/\/ returned results $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['date_added DESC']) ->fetchRecordSet(); Tip: The select() variation gives you access to all the underlying SQL query methods. See Aura\\SqlQuery for more information. Note: If fetchRecordSet() does not find any matches, it will return an empty array. This is important as you cannot call RecordSet methods (see later in the documentation) such as appendNew() or getArrayCopy() on an empty array. In these situations, you must test for the empty array, and then instantiate a new RecordSet, if necessary. See below. <?php $threadRecordSet = $atlas->fetchRecordSet( ThreadMapper::CLASS, [1, 2, 3] ); if (! $threadRecordSet) { $threadRecordSet = $atlas->newRecordSet(ThreadMapper::CLASS); } $threadMapper->appendNew(...); "},{"id":"\/albers\/mapper\/reading.html#4-1-3-2-1","title":"4.1.3.2.1. Accessing\/Reading RecordSet Data","content":"RecordSets act as arrays of Records. As such, you can easily iterate over the RecordSet and access the Records individually.<?php \/\/ fetch the top 100 threads $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['thread_id DESC']) ->limit(100) ->fetchRecordSet(); foreach ($threadRecordSet as $threadRecord) { echo $threadRecord->title; } "},{"id":"\/albers\/mapper\/reading.html#4-1-3-3","title":"4.1.3.3. Fetching Related Records","content":"Any relationships that are set in the Mapper will appear as NULL in the Record object. Related data will only be populated if it is explicitly requested as part of the fetch or select.On a fetch*(), load relateds using a third argument: an array specifying which related fields to retrieve.<?php $threadRecord = $atlas->fetchRecord( ThreadMapper::CLASS, '1', [ 'author', 'summary', 'replies', ] ); $threadRecordSet = $atlas->fetchRecordSet( ThreadMapper::CLASS, [1, 2, 3], [ 'author', 'summary', 'replies', ] ); When using the select() variation, load relateds using the with() method:<?php $threadRecord = $atlas ->select(ThreadMapper::class) ->where('thread_id = ?', '1') ->with([ 'author', 'summary', 'replies', ]) ->fetchRecord(); $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->where('thread_id IN (?)', [1, 2, 3]) ->with([ 'author', 'summary', 'replies', ]) ->fetchRecordSet(); Note: When fetching a manyToMany relationship, you must explicitly specify both the association (through) related AND the manyToMany related. Additionally, you must specify these relationships in the correct order. <?php $threadRecord = $atlas->fetchRecord( ThreadMapper::CLASS, '1', [ 'taggings', \/\/ specify the through first 'tags' \/\/ then the manyToMany ] ); Relationships can be nested as deeply as needed. For example, to fetch the author of each reply on each thread:<?php $threadRecord = $this->atlas ->select(ThreadMapper::class) ->where('thread_id = ?', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author' ] ]) ->fetchRecord(); Alternatively, you can pass a closure to exercise fine control over the query that fetches the relateds:<?php \/\/ fetch thread_id 1; with only the last 10 related replies in descending order; \/\/ including each reply author $threadRecord = $atlas->fetchRecord( ThreadMapper::CLASS, '1', [ 'author', 'summary', 'replies' => function ($selectReplies) { $selectReplies ->limit(10) ->orderBy(['reply_id DESC']) ->with([ 'author' ]); }, ] ); "},{"id":"\/albers\/mapper\/reading.html#4-1-3-3-1","title":"4.1.3.3.1. Accessing\/Reading Related Data","content":"Accessing related data works just like accessing Record properties except instead of using a column name, you use the relationship name defined in the mapper.<?php $threadRecord = $this->atlas ->select(ThreadMapper::class) ->where('thread_id = ?', $threadId) ->with([ 'author', 'summary', 'replies' => [ 'author' ] ]) ->fetchRecord(); \/\/ Assume the author table has a column named `last_name` foreach ($threadRecord->replies as $reply) { echo $reply->author->last_name; } "},{"id":"\/albers\/mapper\/reading.html#4-1-3-4","title":"4.1.3.4. Returning Data in Other Formats","content":"You can return a Record or a RecordSet as an array rather than a Record or RecordSet object using the getArrayCopy() method.<?php $threadRecord = $atlas->fetchRecord('ThreadMapper::CLASS', '1'); $threadArray = $threadRecord->getArrayCopy(); $threadRecordSet = $atlas ->select(ThreadMapper::CLASS) ->orderBy(['date_added DESC']) ->fetchRecordSet(); $threadsArray = $threadRecordSet->getArrayCopy(); JSON-encoding Records and RecordSets is trival.<?php $threadJson = json_encode($threadRecord); $threadsJson = json_encode($threadRecordSet); "},{"id":"\/albers\/mapper\/reading.html#4-1-3-5","title":"4.1.3.5. Reading Record Counts","content":"If you use a select() to fetch a RecordSet with a limit() or page(), you can re-use the select to get a count of how many Records would have been returned. This can be useful for paging displays.<?php $select = $atlas ->select(ThreadMapper::CLASS) ->with([ 'author', 'summary', 'replies' ]) ->limit(10) ->offset(20); $threadRecordSet = $select->fetchRecordSet(); $countOfAllThreads = $select->fetchCount(); "},{"id":"\/albers\/mapper\/records.html#4-1-4-1","title":"4.1.4.1. Creating and Inserting a Record","content":"Create a new Record using the newRecord() method. You can assign data using properties, or pass an array of initial data to populate into the Record.<?php $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', ] ); You can assign a value via a property, which maps to a column name.<?php $date = new \\DateTime(); $thread->date_added = $date->format('Y-m-d H:i:s'); You can insert a single Record back to the database by using the Atlas::insert() method. This will use the appropriate Mapper for the Record to perform the write within a transaction, and capture any exceptions that occur along the way.<?php $success = $atlas->insert($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } Inserting a Record with an auto-incrementing primary key will automatically modify the Record to set the last-inserted ID.Inserting a Record will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.In the following example, assume a Thread Record has a manyToOne relationship with an Author Record using the author_id column. The relationship is named author. (See the section on relationships for more information.)<?php $author = $atlas->fetchRecord(AuthorMapper::CLASS, 4); $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', 'author'=>$author ] ); \/\/ If the insert is successful, the `author_id` column will automatically be \/\/ set to the Author Record's primary key value. In this case, 4. $success = $atlas->insert($thread); echo $thread->author_id; \/\/ 4 Note: If the Author Record is new, Atlas will NOT automatically insert the new Author and set the foreign key on the new Author Record via the insert() method. This can, however, be achieved using the persist() method. This is discussed later in this chapter. The following will fail.<?php $author = $atlas->newRecord(AuthorMapper::CLASS, [ 'first_name'=>'Sterling', 'last_name'=>'Archer' ] ); $thread = $atlas->newRecord(ThreadMapper::CLASS, [ 'title'=>'New Thread Title', 'author'=>$author ] ); \/\/ Insert will not create the related Author Record. Use persist() instead. $success = $atlas->insert($thread); "},{"id":"\/albers\/mapper\/records.html#4-1-4-2","title":"4.1.4.2. Updating an Existing Record","content":"Updating an existing record works the same as insert().<?php \/\/ fetch an existing record by primary key $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); \/\/ Modify the title $thread->title = 'This title is better than the last one'; \/\/ Save the record back to the database. $success = $atlas->update($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } As with insert(), foreign keys are also updated, but only for existing related records.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); $author = $atlas->fetchRecord(AuthorMapper::CLASS, 4); \/\/ Modify the author $thread->author = $author; \/\/ Save the record back to the database. $success = $atlas->update($thread); if ($success) { echo \"Wrote the Record back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } "},{"id":"\/albers\/mapper\/records.html#4-1-4-3","title":"4.1.4.3. Deleting a Record","content":"Deleting a record works the same as inserting or updating.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); $success = $atlas->delete($thread); if ($success) { echo \"Removed record from the database.\"; } else { echo \"Did not remove the Record: \" . $atlas->getException(); } "},{"id":"\/albers\/mapper\/records.html#4-1-4-4","title":"4.1.4.4. Persisting a Record","content":"If you like, you can persist a Record and all of its loaded relationships (and all of their loaded relationships, etc.) back to the database using the Atlas persist() method. This is good for straightforward relationship structures where the order of write operations does not need to be closely managed.The persist() method will: persist many-to-one and many-to-many relateds loaded on the native Record; persist the native Record by ... inserting the Row for the Record if it is new; or, updating the Row for the Record if it has been modified; or, deleting the Row for the Record if the Record has been marked for deletion using the Record::markForDeletion() method; persist one-to-one and one-to-many relateds loaded on the native Record. <?php $success = $atlas->persist($record); if ($success) { echo \"Wrote the Record and all of its relateds back to the database.\"; } else { echo \"Did not write the Record: \" . $atlas->getException(); } As with insert and update, this will automatically set the foreign key fields on the native Record, and on all the loaded relationships for that Record.If a related field is not loaded, it cannot be persisted automatically.Note that whether or not the Row for the Record is inserted\/updated\/deleted, the persist() method will still recursively traverse all the related fields and persist them as well.The delete() method will not attempt to cascade deletion or nullification across relateds at the ORM level. Your database may have cascading set up at the database level; Atlas has no control over this."},{"id":"\/albers\/mapper\/records.html#4-1-4-5","title":"4.1.4.5. Marking Records for Deletion","content":"You may also mark records for deletion and they will be removed from the database after a transaction is completed via persist().<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3); \/\/ Mark the record for deletion $thread->markForDeletion(); $atlas->persist($thread); You can also mark several related Records for deletion and when the native Record is persisted, they will be deleted from the database.<?php \/\/ Assume a oneToMany relationship between a thread and its comments \/\/ Select the thread and related comments $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 3, [ 'comments' ] ); \/\/ Mark each related comment for deletion foreach ($thread->comments as $comment) { $comment->markForDeletion(); } \/\/ Persist the thread and the comments are also deleted $atlas->persist($thread); "},{"id":"\/albers\/mapper\/record-sets.html#4-1-5-1","title":"4.1.5.1. New RecordSets","content":"Create a new RecordSet using the newRecordSet() method.<?php $threadRecordSet = $atlas->newRecordSet(ThreadMapper::CLASS); "},{"id":"\/albers\/mapper\/record-sets.html#4-1-5-2","title":"4.1.5.2. Appending Records to a RecordSet","content":"You can append a new Record to an existing RecordSet using appendNew(), optionally passing any data you want to initially populate into the Record:<?php $newThread = $threadRecordSet->appendNew([ 'title' => 'New Title', ]); Additionally, you can append foreign Records to a native Record's relateds.<?php $thread = $atlas->fetchRecord(ThreadMapper::CLASS, 1, [ 'comments', ] ); \/\/ Ensure we have a RecordSet to append to if (! $thread->comments) { $thread->comments = $atlas->newRecordSet(CommentMapper::CLASS); } $comment = $thread->comments->appendNew([ 'thread_id' => $thread->thread_id, 'comment' => 'Lorem ipsum dolor sit amet...' ]); \/\/ plan to insert the new comment $transaction->insert($comment); \/\/ Or persist the thread $atlas->persist($thread); "},{"id":"\/albers\/mapper\/record-sets.html#4-1-5-3","title":"4.1.5.3. Array Access","content":"The RecordSet also acts as an array, so you can get\/set\/unset Records by their sequential keys in the RecordSet.<?php \/\/ address the second record in the set $threadRecordSet[1]->title = 'Changed Title'; \/\/ unset the first record in the set unset($threadRecordSet[0]); \/\/ push a new record onto the set $threadRecordSet[] = $atlas->newRecord(ThreadMapper::CLASS); "},{"id":"\/albers\/mapper\/record-sets.html#4-1-5-4","title":"4.1.5.4. Searching within RecordSets","content":"You can search for Records within an existing RecordSet by their column values:<?php $threadRecordSet = $atlas->select(ThreadMapper::CLASS) ->where('published=?', 1) ->fetchRecordSet(); \/\/ returns one matching Record object from the RecordSet, \/\/ or false if there is no match $matchingRecord = $threadRecordSet->getOneBy(['subject' => 'Subject One']); \/\/ returns an array of matching Record objects from the RecordSet $matchingRecords = $threadRecordSet->getAllBy(['author_id' => '5']); "},{"id":"\/albers\/mapper\/record-sets.html#4-1-5-5","title":"4.1.5.5. Removing Records from RecordSets","content":"You can remove Records from a RecordSet by their column values. This does NOT delete the Record from the database; only from the RecordSet.<?php \/\/ unsets and returns one matching Record from the Record Set, \/\/ or false if there is no match $removedRecord = $threadRecordSet->removeOneBy(['subject' => 'Subject One']); \/\/ unsets and returns an array of matching Record objects from the Record Set $removedRecords = $threadRecordSet->removeAllBy(['author_id' => '5']); Note: This only removes them from the RecordSet; it does not delete them from the database. If you need to delete a record from the database, see the sections on Marking Records for Deletion and deleting Records. "},{"id":"\/albers\/mapper\/transactions.html#4-1-6","title":"4.1.6. Transactions (Unit of Work)","content":"If you make changes to several Records, you can write them back to the database using a unit-of-work Transaction. You can plan for Records to be inserted, updated, and deleted, in whatever order you like, and then execute the entire transaction plan at once. Exceptions will cause a rollback.<?php \/\/ create a transaction $transaction = $atlas->newTransaction(); \/\/ plan work for the transaction $transaction->insert($record1); $transaction->update($record2); $transaction->delete($record3); \/\/ or persist an entire record and its relateds $transaction->persist($record4); \/\/ execute the transaction plan $success = $transaction->exec(); if ($success) { echo \"The Transaction succeeded!\"; } else { \/\/ get the exception that was thrown in the transaction $e = $transaction->getException(); \/\/ get the work element that threw the exception $work = $transaction->getFailure(); \/\/ some output echo \"The Transaction failed: \"; echo $work->getLabel() . ' threw ' . $e->getMessage(); } "},{"id":"\/albers\/mapper\/behavior.html#4-1-7","title":"4.1.7. Record and RecordSet Behaviors","content":"Atlas makes it easy to add your own behaviors to both Records and RecordSets. To accomplish this, you need a Record class for custom Record logic, and a RecordSet class for custom RecordSet logic. The Atlas CLI script (installable via composer using atlas\/cli), can create these classes for you, saving you from manually writing them.Consult the Atlas CLI documentation.It's important to note that the Record and RecordSet objects described below should only be used for very simple behaviors. Any non-trivial domain work may be an indication that you need a domain layer. See the documentation on Domain Models for examples of how you can use Atlas to build a domain layer.Here is an example using the atlas\/cli package and the --full option..\/vendor\/bin\/atlas-skeleton.php \\ --conn=\/path\/to\/conn.php \\ --dir=src\/App\/DataSource \\ --table=threads \\ --full \\ App\\\\DataSource\\\\Thread Upon completion, you will have a folder layout as follows:-- src -- App -- DataSource -- Thread -- ThreadMapper.php -- ThreadMapperEvents.php -- ThreadRecord.php -- ThreadRecordSet.php -- ThreadTable.php -- ThreadTableEvents.php Once you have a Record Class (ThreadRecord.php), you can create custom methods to call from your Record object.<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\Record; \/** * @inheritdoc *\/ class ThreadRecord extends Record { \/\/ Format the date_created property public function formatDate($format = 'M jS, Y') { $dateTime = new \\DateTime($this->date_created); return $dateTime->format($format); } } $thread = $atlas->fetchRecord(ThreadMapper::CLASS, $id); echo $thread->formatDate(); \/\/ outputs something like `Aug 21st, 2017` The same concept is available for RecordSets using the RecordSet class. In our example ThreadRecordSet.php.<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\RecordSet; \/** * @inheritdoc *\/ class ThreadRecordSet extends RecordSet { public function foo() { $data = [] foreach ($this as $record) { $data[] = $record->title; } return implode('; ', $data); } } $threads = $atlas->fetchRecordSet(ThreadMapper::CLASS, [1, 2, 3]); echo $threads->foo(); "},{"id":"\/albers\/mapper\/events.html#4-1-8-1","title":"4.1.8.1. Record Events","content":"There are several events that will automatically be called when interacting with a Record object. If you used the Atlas CLI tool with the --full option, a MapperEvents class will be created for you. For example, ThreadMapperEvents.php. With this class, you can override any of the available mapper events."},{"id":"\/albers\/mapper\/events.html#4-1-8-1-1","title":"4.1.8.1.1. Available Events","content":"The insert(), update(), and delete() methods all have 3 events associated with them. A before*(), a modify*(), and an after*().<?php \/\/ Runs before the Insert object is created beforeInsert(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Insert object is created, but before it is executed modifyInsert(MapperInterface $mapper, RecordInterface $record, Insert $insert) \/\/ Runs after the Insert object is executed afterInsert(MapperInterface $mapper, RecordInterface $record, Insert $insert, PDOStatement $pdoStatement) \/\/ Runs before the Update object is created beforeUpdate(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Update object is created, but before it is executed modifyUpdate(MapperInterface $mapper, RecordInterface $record, Update $update) \/\/ Runs after the Update object is executed afterUpdate(MapperInterface $mapper, RecordInterface $record, Update $update, PDOStatement $pdoStatement) \/\/ Runs before the Delete object is created beforeDelete(MapperInterface $mapper, RecordInterface $record) \/\/ Runs after the Delete object is created, but before it is executed modifyDelete(MapperInterface $mapper, RecordInterface $record, Delete $delete) \/\/ Runs after the Delete object is executed afterDelete(MapperInterface $mapper, RecordInterface $record, Delete $delete, PDOStatement $pdoStatement) Here is a simple example with the assumption that the Record object has a validate() method and a getErrors() method. See the section on Adding Logic to Records and RecordSets.<?php namespace Blog\\DataSource\\Posts; use Atlas\\Orm\\Mapper\\MapperEvents; use Atlas\\Orm\\Mapper\\MapperInterface; use Atlas\\Orm\\Mapper\\RecordInterface; use Atlas\\Orm\\Exception; \/** * @inheritdoc *\/ class PostsMapperEvents extends MapperEvents { public function beforeUpdate(MapperInterface $mapper, RecordInterface $record) { if (! $record->validate()) throw new Exception('Update Error'); } } } And you might have something like this in your code:<?php $success = $atlas->update($post); if ($sucess) { echo \"Post updated\"; } else { foreach ($post->getErrors as $error) { echo $error . '<br\/>'; } } "},{"id":"\/albers\/mapper\/direct.html#4-1-9","title":"4.1.9. Direct Queries","content":"If you need to perform queries directly, additional fetch* and yield* methods are provided which expose the Extended PDO functionality. By using the $cols parameter, you can select specific columns or individual values. For example:<?php \/\/ an array of IDs $threadIds = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id']) ->limit(10) ->orderBy('thread_id DESC') ->fetchCol(); \/\/ key-value pairs of IDs and titles $threadIdsAndTitles = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'tite']) ->limit(10) ->orderBy('thread_id DESC') ->fetchPairs(); \/\/ etc. See the list of ExtendedPdo::fetch*() and yield*() methods for more.You can also call fetchRow() or fetchRows() to get Row objects directly from the Table underlying the Mapper."},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-1","title":"4.1.9.1.1. Fetch Value","content":"Returns a single value, or false.<?php $subject = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->where('thread_id = ?', '1') ->fetchValue(); \/\/ \"Subject One\" "},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-2","title":"4.1.9.1.2. Fetch Column","content":"Returns a sequential array of one column, or an empty array.<?php $subjects = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->limit(2) ->fetchCol(); \/\/ [ \/\/ 0 => \"Subject One\", \/\/ 1 => \"Subject Two\" \/\/ ] "},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-3","title":"4.1.9.1.3. Fetch Pairs","content":"Returns an associative array where the key is the first column and the value is the second column, or an empty array.<?php $subjectAndBody = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->fetchPairs(); \/\/ [ \/\/ 'Subject One' => \"Body Text One\", \/\/ 'Subject Two' => \"Body Text Two\" \/\/ ] "},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-4","title":"4.1.9.1.4. Fetch One","content":"Returns an associative array of one row, or false.<?php $threadData = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body', 'author_id']) ->where('thread_id = 1') ->fetchOne(); \/\/ [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\", \/\/ 'author_id' => \"1\" \/\/ ] "},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-5","title":"4.1.9.1.5. Fetch Assoc","content":"Returns an associative array of rows keyed on the first column specified, or an empty array.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->fetchAssoc(); \/\/ [ \/\/ 'Subject One' => [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\", \/\/ ], \/\/ 'Subject Two' => [ \/\/ 'subject' => \"Subject Two\", \/\/ 'body' => \"Body Text Two\" \/\/ ] \/\/ ] "},{"id":"\/albers\/mapper\/direct.html#4-1-9-1-6","title":"4.1.9.1.6. Fetch All","content":"Returns a sequential array of associative arrays, or an empty array.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->limit(2) ->orderBy('thread_id DESC') ->fetchAll(); \/\/ [ \/\/ 0 => [ \/\/ 'subject' => \"Subject One\", \/\/ 'body' => \"Body Text One\" \/\/ ], \/\/ 1 => [ \/\/ 'subject' => \"Subject Two\", \/\/ 'body' => \"Body Text Two\" \/\/ ] \/\/ ] "},{"id":"\/albers\/mapper\/direct.html#4-1-9-2","title":"4.1.9.2. Yielding Data","content":"If you prefer to get the results one at a time, you can use the yield* variations on these methods to iterate through the result set instead of returning an array."},{"id":"\/albers\/mapper\/direct.html#4-1-9-2-1","title":"4.1.9.2.1. Yield Col","content":"Iterate through a sequential array of one column.<?php $subjects = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject']) ->yieldCol(); foreach($subjects as $subject) { echo $subject; } "},{"id":"\/albers\/mapper\/direct.html#4-1-9-2-2","title":"4.1.9.2.2. Yield Pairs","content":"Iterate through an associative array by the first column specified.<?php $subjectAndBody = $atlas ->select(ThreadMapper::CLASS) ->cols(['subject', 'body']) ->yieldPairs(); foreach($subjectAndBody as $subject => $body) { echo $subject . \": \" . $body; } "},{"id":"\/albers\/mapper\/direct.html#4-1-9-2-3","title":"4.1.9.2.3. Yield Assoc","content":"Iterate through an associative array of rows by the first column specified.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'subject']) ->yieldAssoc(); foreach($threads as $threadId => $thread) { echo $threadId . \": \" . $thread['subject']; } "},{"id":"\/albers\/mapper\/direct.html#4-1-9-2-4","title":"4.1.9.2.4. Yield All","content":"Iterate through a sequential array of rows.<?php $threads = $atlas ->select(ThreadMapper::CLASS) ->cols(['thread_id', 'subject']) ->yieldAll(); foreach($threads as $thread) { echo $thread['thread_id'] . \": \" . $thread['subject']; } "},{"id":"\/albers\/mapper\/direct.html#4-1-9-3","title":"4.1.9.3. Complex Queries","content":"You can use any of the direct table access methods with more complex queries and joins.<?php $threadData = $atlas ->select(ThreadMapper::CLASS) ->cols(['threads.subject', 'authors.name', 's.*']) ->join('INNER', 'authors', 'authors.author_id = threads.author_id') ->join('INNER', 'summary s', 's.thread_id = threads.thread_id') ->where('authors.name = ?', $name) ->orderBy('threads.thread_id DESC') ->offset(2) ->limit(2) ->fetchAssoc(); "},{"id":"\/albers\/mapper\/direct.html#4-1-9-4","title":"4.1.9.4. Reusing the Select","content":"The select object can be used for multiple queries, which may be useful for pagination. The generated select statement can also be displayed for debugging purposes.<?php $select = $atlas ->select(ThreadMapper::CLASS) ->cols(['*']) ->offset(10) ->limit(5); \/\/ Fetch the current result set $results = $select->fetchAll(); \/\/ Fetch the row count without any limit or offset $totalCount = $select->fetchCount(); \/\/ View the generated select statement $statement = $select->getStatement(); "},{"id":"\/albers\/mapper\/domain.html#4-1-10","title":"4.1.10. Domain Models","content":"You can go a long way with just your persistence model Records. However, at some point you may want to separate your persistence model Records from your domain model Entities and Aggregates. This section offers some suggestions and examples on how to do that."},{"id":"\/albers\/mapper\/domain.html#4-1-10-1","title":"4.1.10.1. Persistence Model","content":"For the examples below, we will work with an imaginary forum application that has conversation threads. The ThreadMapper might something like this:<?php namespace App\\DataSource\\Thread; use App\\DataSource\\Author\\AuthorMapper; use App\\DataSource\\Summary\\SummaryMapper; use App\\DataSource\\Reply\\ReplyMapper; use App\\DataSource\\Tagging\\TaggingMapper; use App\\DataSource\\Tag\\TagMapper; use Atlas\\Orm\\Mapper\\AbstractMapper; class ThreadMapper extends AbstractMapper { protected function setRelated() { $this->manyToOne('author', AuthorMapper::CLASS); $this->oneToOne('summary', SummaryMapper::CLASS); $this->oneToMany('replies', ReplyMapper::CLASS); $this->oneToMany('taggings', TaggingMapper::CLASS); $this->manyToMany('tags', TagMapper::CLASS, 'taggings'); } } (We will leave the other mappers and their record classes for the imagination.)"},{"id":"\/albers\/mapper\/domain.html#4-1-10-2","title":"4.1.10.2. Domain Model Interfaces","content":"At some point, we have decided we want to depend on domain Entities or Aggregates, rather than persistence Records, in our application.For example, the interface we want to use for a Thread Entity in domain might look like this:<?php namespace App\\Domain\\Thread; interface ThreadInterface { public function getId(); public function getSubject(); public function getBody(); public function getDatePublished(); public function getAuthorId(); public function getAuthorName(); public function getTags(); public function getReplies(); } (This interface allows us to typehint the application against these domain- specific Entity methods, rather than using the persistence Record properties.)Further, we will presume a naive domain repository implementation that returns Thread Entities. It might look something like this:<?php namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadMapper; class ThreadRepository { protected $mapper; public function __construct(ThreadMapper $mapper) { $this->mapper = $mapper; } public function fetchThread($thread_id) { $record = $this->mapper->fetchRecord($thread_id, [ 'author', 'taggings', 'tags', 'replies', ]); return $this->newThread($record); } protected function newThread(ThreadRecord $record) { \/* ??? *\/ } } The problem now is the newThread() factory method. How do we convert a persistence layer ThreadRecord into a domain layer ThreadInterface implementation?There are three options, each with different tradeoffs: Implement the domain interface in the persistence layer. Compose the persistence record into the domain object. Map the persistence record fields to domain implementation fields. "},{"id":"\/albers\/mapper\/domain.html#4-1-10-3","title":"4.1.10.3. Implement Domain In Persistence","content":"The easiest thing to do is to implement the domain ThreadInterface in the persistence ThreadRecord, like so:<?php namespace App\\DataSource\\Thread; use Atlas\\Orm\\Mapper\\Record; use App\\Domain\\Thread\\ThreadInterface; class ThreadRecord extends Record implements ThreadInterface { public function getId() { return $this->thread_id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->date_published; } public function getAuthorId() { return $this->author->author_id; } public function getAuthorName() { return $this->author->name; } public function getTags() { return $this->tags->getArrayCopy(); } public function getReplies() { return $this->replies->getArrayCopy(); } } With this, the ThreadRepository::newThread() factory method doesn't actually need to factory anything at all. It just returns the persistence record, since the record now has the domain interface.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return $record; } Pros: Trivial to implement. Cons: Exposes the persistence layer Record methods and properties to the domain layer, where they can be easily abused. "},{"id":"\/albers\/mapper\/domain.html#4-1-10-4","title":"4.1.10.4. Compose Persistence Into Domain","content":"Almost as easy, but with better separation, is to have a domain layer object that implements the domain interface, but encapsulates the persistence record as the data source. The domain object might look something like this:<?php namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadRecord; class Thread implements ThreadInterface { protected $record; public function __construct(ThreadRecord $record) { $this->record = $record; } public function getId() { return $this->record->thread_id; } public function getTitle() { return $this->record->title; } public function getBody() { return $this->record->body; } public function getDatePublished() { return $this->record->date_published; } public function getAuthorId() { return $this->record->author->author_id; } public function getAuthorName() { return $this->record->author->name; } public function getTags() { return $this->record->tags->getArrayCopy(); } public function getReplies() { return $this->record->replies->getArrayCopy(); } } Now the ThreadRepository::newThread() factory method has to do a little work, but not much. All it needs is to create the Thread domain object with the ThreadRecord as a constructor dependency.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return new Thread($record); } Pros: Hides the persistence record behind the domain interface. Easy to implement. Cons: The domain object is now dependent on the persistence layer, which is not the direction of dependencies we'd prefer. "},{"id":"\/albers\/mapper\/domain.html#4-1-10-5","title":"4.1.10.5. Map From Persistence To Domain","content":"Most difficult, but with the best separation, is to map the individual parts of the persistence record over to a \"plain old PHP object\" (POPO) in the domain, perhaps something like the following:<?php namespace App\\Domain\\Thread; use App\\DataSource\\Thread\\ThreadRecord; class Thread implements ThreadInterface { protected $id; protected $title; protected $body; protected $datePublished; protected $authorId; protected $authorName; protected $tags; protected $replies; public function __construct( $id, $title, $body, $datePublished, $authorId, $authorName, array $tags, array $replies ) { $this->id = $id; $this->title = $title; $this->body = $body; $this->datePublished = $datePublished; $this->authorId = $authorId; $this->authorName = $authorName; $this->tags = $tags; $this->replies = $replies; } public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getBody() { return $this->body; } public function getDatePublished() { return $this->datePublished; } public function getAuthorId() { return $this->authorId; } public function getAuthorName() { return $this->authorName; } public function getTags() { return $this->tags; } public function getReplies() { return $this->replies; } } Now the ThreadRepository::newThread() factory method has a lot of work to do. It needs to map the individual fields in the persistence record to the domain object properties.<?php class ThreadRepository ... protected function newThread(ThreadRecord $record) { return new Thread( $record->thread_id, $record->title, $record->body, $record->date_published, $record->author->author_id, $record->author->name, $record->tags->getArrayCopy(), $record->replies->getArrayCopy() ); } Pros: Offers true separation of domain from persistence. Cons: Tedious and time-consuming to implement. "},{"id":"\/albers\/mapper\/domain.html#4-1-10-6","title":"4.1.10.6. Which Approach Is Best?","content":"\"It depends.\" What does it depend on? How much time you have available, and what kind of suffering you are willing to put up with.If you need something quick, fast, and in a hurry, implementing the domain interface in the persistence layer will do the trick. However, it will come back to bite in you just as quickly, as you begin to realize that you need different domain behaviors in different contexts, all built from the same backing persistence records.If you are willing to deal with the trouble that comes from depending on the persistence layer records inside your domain, and the possibility that other developers will expose the underlying record in subtle ways, then composing the record into the domain may be your best bet.The most formally-correct approach is to map the record fields over to domain object properties. This level of separation makes testing and modification of application logic much easier in the long run, but it takes a lot of time, attention, and discipline."},{"id":"\/albers\/skeleton\/getting-started.html#4-2-1","title":"4.2.1. Atlas.Cli 1.x","content":"This is the command-line interface package for Atlas. It is intended for use in your development environments, not your production ones."},{"id":"\/albers\/skeleton\/getting-started.html#4-2-1-1","title":"4.2.1.1. Installation","content":"This package is installable and autoloadable via Composer as atlas\/cli.Add it to the require-dev section of your root-level composer.json to install the atlas-skeleton command-line tool.{ \"require-dev\": { \"atlas\/cli\": \"~1.0\" } } "},{"id":"\/albers\/skeleton\/getting-started.html#4-2-1-2","title":"4.2.1.2. Creating Skeleton Classes","content":"You can create your data source classes by hand, but it's going to be tedious to do so. Instead, use the atlas-skeleton command to read the table information from the database.Create a PHP file to return an array of connection parameters suitable for PDO:<?php \/\/ \/path\/to\/conn.php return [ 'mysql:dbname=testdb;host=localhost', 'username', 'password' ]; ?> You can then invoke the skeleton generator using that connection. Specify a target directory for the skeleton files, and pass the namespace name for the data source classes. You can pass an explicit table name to keep the generator from trying to guess the name..\/vendor\/bin\/atlas-skeleton.php \\ --conn=\/path\/to\/conn.php \\ --dir=src\/App\/DataSource \\ --table=threads \\ App\\\\DataSource\\\\Thread N.b.: The backslashes (\\) at the end of the lines are to allow the command to be split across multiple lines in Unix. If you are on Windows, omit the trailing backslashes and enter the command on a single line. That will create this directory and two classes in src\/App\/DataSource\/:\u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 Thread \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u009c\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadMapper.php \u00c2\u00a0\u00c2\u00a0 \u00e2\u0094\u0094\u00e2\u0094\u0080\u00e2\u0094\u0080 ThreadTable.php The Mapper class will be empty, and the Table class will a description of the specified --table. Note that you should not make changes to the table class, as they will be overwritten if you regenerate the skeleton.Do that once for each SQL table in your database.If you pass --full to atlas-skeleton, it will additionally generate empty MapperEvents, Record, RecordSet, and TableEvents classes. (These are useful only if you want to add custom behaviors, and will not be overwritten if you regenerate the skeleton.)The --full option will also add a Fields class with @property annotations for table columns on the Record. This file will be overwritten if the table class is regenerated, as it is defined from the table columns (which may have changed since the last skeleton generation)."}]