diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e9b977 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*~ +vendor/ +states/ +nbproject/private +*.sqlite3 +.DS_Store +composer.5.json +composer.lock + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4a05e59 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: php +php: [7.3] +before_script: + - composer self-update -q + - if [ -n "$GITHUB_TOKEN" ]; then composer config github-oauth.github.com ${GITHUB_TOKEN}; fi; + - composer -vvv update --prefer-dist + - composer show -i +script: php "okay/Sql.ok/_ok.php" -I diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62cf419 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Keith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1fac55 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) +[![Build Status](https://travis-ci.com/keithy/primo-pisara-php.svg?branch=master)](https://travis-ci.com/keithy/primo-pisara-php) +[![GitHub issues](https://img.shields.io/github/issues/keithy/primo-pisara-php.svg)](https://github.com/keithy/primo-pisara-php/issues) +[![Latest Version](https://img.shields.io/github/release/keithy/primo-pisara-php.svg)](https://github.com/keithy/primo-pisara-php/releases) +[![PHP from Travis config](https://img.shields.io/travis/php-v/keithy/primo-pisara-php.svg)](https://travis-ci.com/keithy/primo-pisara-php) + +## primo-pisara-php + +### Persistence I/O Stage Actors Roles => Architecture + +*Applying the DCI Architecture to create a PHP Framework for **Uncle Bob** to be proud of.* + +### Primary Goal: + +1. The WEB is an I/O device! - Slim/PSR7 etc. is a plugin. +2. The database is a plugin. +3. Develop and Organize in UseCases - "**Stages**", upon which the **Actors** perform. + +---- + +* P - Persistence +* I - I/O +* S - Stages +* A - Actors +* R - Roles +* A - Architecture diff --git a/actors/StdClass.php b/actors/StdClass.php new file mode 100644 index 0000000..8cc9df3 --- /dev/null +++ b/actors/StdClass.php @@ -0,0 +1,13 @@ + Architecture", + "keywords": [ + "pdo", + "psr7", + "architecture" + ], + "homepage": "https:\/\/github.com\/keithy\/primo-pisara-php", + "license": "MIT", + "authors": [ + { + "name": "Keithy", + "email": "keithy@consultant.com", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "https:\/\/github.com\/keithy\/primo-pisara-php.git" + }, + { + "type": "vcs", + "url": "https:\/\/github.com\/keithy\/okay-php.git" + }, + { + "type": "vcs", + "url": "https:\/\/github.com\/keithy\/dci-php.git" + }, + { + "type": "vcs", + "url": "https:\/\/github.com\/keithy\/primo-pdo-php.git" + } + ], + "require": { + "mbrowne\/dci": "dev-kphmods", + "primo\/pdo": "dev-master" + }, + "require-dev": { + "robmorgan\/phinx": "^0.10.7", + "fzaninotto\/faker": "^1.8", + "okay\/okay": "dev-master" + }, + "autoload": { + "psr-4": { + "Persistence\\": "src\/Persistence", + "Actor\\": "src\/actors" + }, + "classmap": [ + "persistence\/", + "io\/", + "stages\/", + "actors\/" + ] + }, + "autoload-dev": {}, + "scripts": {} +} \ No newline at end of file diff --git a/io/PSR7_Json.php b/io/PSR7_Json.php new file mode 100644 index 0000000..dc2fc50 --- /dev/null +++ b/io/PSR7_Json.php @@ -0,0 +1,264 @@ +state = $state; + $this->controller = $replyController->entering($this)->addRole('Controller_To_HTTP', $this); + } + + function setIO($request, $response) + { + $this->request = $request; + $this->response = $response; + return $this; + } + + // ROLES ALLOCATION + //requestRole = //teachMeHowTo + function teachMeHowTo_readFile_ofType($controller, $type) + { + return $controller->addRole("ReadFile_$type", $this); + } + + function respondNotFound($info) + { + $this->controller->statusNotFound(); + return $this->respondErrors("Not Found", $info); + } + + function respondInvalidParameter($info) + { + $this->controller->statusInvalidParameter(); + + $info['type'] = $this->controller->fakeMockRealType; + $info['database-config'] = $this->state->dbConfigFile; + $info['log'] = $this->log; + + return $this->respondErrors("Invalid or Missing Parameter", $info); + } + + function respondNoData($info) + { + $this->controller->statusNoData(); + + $info['type'] = $this->controller->fakeMockRealType; + $info['database-config'] = $this->state->dbConfigFile; + $info['log'] = $this->log; + + return $this->respondErrors("No Data", $info); + } + + function respondErrors($message, $info = []) + { + $errors = []; + $info['message'] = $message; + + if ($this->state->configAt('settings')['displayErrorDetails'] ?? false) { + // $info['type'] = $this->controller->fakeMockRealType; + $info['database-config'] = $this->state->dbConfigFile; + // $info['log'] = $this->log; + } + + $errors['errors'] = $info; + + return $this->controller->respond($errors); + } + + function respond($data) + { + return $this->controller->respond($data); + } + + function setResponseStatus($code) + { + $this->response = $this->response->withStatus($code); + } + + function reply($data) + { + return $this->response + ->write(json_encode($data)) + ->withHeader('Content-Type', 'application/json'); + } + } + +} + +/** + * Roles are defined in a sub-namespace of the context as a workaround for the fact that + * PHP doesn't support inner classes. + * + * We use the trait keyword to define roles because the trait keyword is native to PHP (PHP 5.4+). + * In an ideal world it would be better to have a "role" keyword -- think of "trait" as just + * our implementation technique for roles in PHP. + * (This particular implmentation for PHP actually uses a separate class for the role behind the scenes, + * but that programmer needn't be aware of that.) + */ + +namespace IO\PSR7_Json\Roles { + + + trait Controller_To_HTTP + { + + function respond($data) + { + switch (true) { + case (is_array($data)); + $reply = $data; + $reply['success'] = true; + case (true === $data); + $reply = ["success" => true]; + break; + case (false === $data); + $reply = ["success" => false]; + break; + } + if ($this->message) { + $reply['message'] = $this->message; + } + return $this->context->reply($reply); + } + + function statusOK($message = null) // the slim default - so probably not needed + { + $this->withMessage($message); + $this->context->setResponseStatus(200); + } + + function statusNoData($message = null) + { + $this->withMessage($message); + $this->context->setResponseStatus(204); + } + + function statusInvalidParameter($message = null) + { + $this->withMessage($message); + $this->context->setResponseStatus(400); + } + + function statusNotFound($message = null) + { + $this->withMessage($message); + $this->context->setResponseStatus(404); + } + + function statusInvalidFile($message = null) + { + $this->withMessage($message); + $this->context->setResponseStatus(404); + } + } + + trait ReadFile_json + { + + function readFormat() + { + return 'json'; + } + + function readFor($path) + { + return json_decode(file_get_contents($path), true); + } + } + + trait ReadFile_yml + { + + function readFormat() + { + return 'yml'; + } + + function readFor($path) + { + + return yaml_parse_file(a_path($path)); + } + } + + trait ReadFile_php + { + + function readFormat() + { + return "php"; + } + + function readFor($path) + { + try { + return require($path); + } catch (\Exception $ex) { + $this->statusInvalidFile(); + $this->context->respondErrors($ex->getMessage()); + } + } + } + + trait ReadFile_inc + { + + function readFormat() + { + return "php"; + } + + function readFor($path) + { + try { + return require($path); + } catch (\Exception $ex) { + $this->statusInvalidFile(); + $this->context->respondErrors($ex->getMessage()); + } + } + } + + trait ReadFile_json5 + { + + function readFormat() + { + return "json5"; + } + + function readFor($path) + { + try { + return json5_decode(file_get_contents($path)); + // json5_decode raises Exceptions on parsing problems + } catch (\SyntaxError $ex) { + $this->statusInvalidFile(); + $this->context->respondErrors($ex->getMessage()); + } + } + } + +} \ No newline at end of file diff --git a/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/TestClass.php b/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/TestClass.php new file mode 100644 index 0000000..685e554 --- /dev/null +++ b/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/TestClass.php @@ -0,0 +1,9 @@ +withTable('tests', 'id', 'name'); + + require_once __DIR__ . "/TestClass.php"; + +} + +namespace ok { + + EXPECT("fetchAll()"); + + _('to retrieve 5 items into instances of Actor\StdClass'); + + $set = $db->fetchAll(); + + assert(5 == count($set), count($set)); + + assert('Actor\StdClass' == get_class($set[0])); + + _('but only have the two properties'); + + assert(2 == count(array_keys($set[0]->toArray())), count(array_keys((array) $set[0]))); +} + +namespace ok { + + EXPECT("fetchAll('ok\TestClass')"); + + _('to retrieve 5 items into instances of ok\TestClass'); + + $set = $db->fetchAll('ok\TestClass'); + + assert(5 == count($set), count($set)); + + assert('ok\TestClass' == get_class($set[0])); +} + +namespace ok { + + EXPECT("fetchAll('ok\TestClass', 'name')"); + + _('to retrieve 5 items into instances of ok\TestClass'); + + $set = $db->fetchAll('ok\TestClass', 'name'); + + assert(5 == count($set), count($set)); + + assert('ok\TestClass' == get_class($set[0])); + + _('but only have the one property'); + + assert(1 == count(array_keys($set[0]->toArray())), count(array_keys((array) $set[0]))); +} + diff --git a/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/focusing on the Table.inc b/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/focusing on the Table.inc new file mode 100644 index 0000000..3805206 --- /dev/null +++ b/okay/Sql.ok/DB(seeded:tests:5).ok/& Sql Persistence.ok/focusing on the Table.inc @@ -0,0 +1,55 @@ +withTable('tests', 'id'); + + require_once __DIR__ . "/TestClass.php"; + +} + +namespace ok { + + EXPECT("fetchAll()"); + + _('to retrieve 5 items into instances of DCI\StdClass'); + + $set = $db->fetchAll(); + + assert(5 == count($set), count($set)); + + assert('Actor\StdClass' == get_class($set[0])); +} + +namespace ok { + + EXPECT("fetchAll('ok\TestClass')"); + + _('to retrieve 5 items into instances of Phianola\Woogie\OK\TestClass'); + + $set = $db->fetchAll('ok\TestClass'); + + assert(5 == count($set), count($set)); + + assert('ok\TestClass' == get_class($set[0])); +} + +namespace ok { + + EXPECT("fetchAll('ok\TestClass', 'name')"); + + _('to retrieve 5 items into instances of Phianola\Woogie\OK\TestClass'); + + $set = $db->fetchAll('ok\TestClass', 'name'); + + assert(5 == count($set), count($set)); + + assert('ok\TestClass' == get_class($set[0])); + + _('but only have the one property'); + + assert(1 == count(array_keys($set[0]->toArray())), count(array_keys((array) $set[0]))); +} + diff --git a/okay/Sql.ok/DB(seeded:tests:5).ok/ModelWithQueryRole.ok/Tests.inc b/okay/Sql.ok/DB(seeded:tests:5).ok/ModelWithQueryRole.ok/Tests.inc new file mode 100644 index 0000000..abb8f17 --- /dev/null +++ b/okay/Sql.ok/DB(seeded:tests:5).ok/ModelWithQueryRole.ok/Tests.inc @@ -0,0 +1,94 @@ +withTable('tests', 'id'); + + $test = new \Actor\StdClass; + $test->id = 1; + + $db->useAsQuery($test); + + _("to be able to query Tests - by id"); + + $set = $test->fetch(); + + assert(1 == count($set)); +} + +namespace ok { + + EXPECT("model created with query-role added initially"); + + $db = $PERSISTENCE->withTable('tests', 'id'); + + $test = $db->useAsQuery(new \Actor\StdClass); + + $test->id = 1; + + _("to be able to query Tests - by id"); + + $set = $test->fetch(); + + assert(1 == count($set)); +} + +namespace ok { + + EXPECT("empty model with query-role provided by the context"); + + $db = $PERSISTENCE->withTable('tests', 'id'); + + $test = $db->newQuery(); + + $test->id = 1; + + _("to be able to query Tests - by id"); + + $set = $test->fetch(); + + assert(1 == count($set)); +} + +namespace ok { + + EXPECT("using model with query-role to delete a row by id"); + + $db = $PERSISTENCE->withTable('tests', 'id'); + + $test = $db->newQuery(); + $test->id = 1; + + _("to return true -> success"); + + $result = $test->delete(); + + assert(true == $result); + + _("and a query to return empty"); + + $set = $test->fetch(); + + assert(0 == count($set)); +} + +namespace ok { + + EXPECT("to be able to fetch all Tests"); + + $db = $PERSISTENCE->withTable('tests', 'id'); + + $test = new \Actor\StdClass; + + $set = $db->useAsQuery($test)->fetchAll(); + + assert(4 == count($set)); +} diff --git a/okay/Sql.ok/DB(seeded:tests:5).ok/_setup.php b/okay/Sql.ok/DB(seeded:tests:5).ok/_setup.php new file mode 100644 index 0000000..9de0a16 --- /dev/null +++ b/okay/Sql.ok/DB(seeded:tests:5).ok/_setup.php @@ -0,0 +1,11 @@ +choose('seeded')->which('snapshots')->copyTo($READER->choose('seeded')); + +$PERSISTENCE = new Persistence\Sql( $environment->pdo() ); + diff --git a/okay/Sql.ok/_fixtures/migrations/0001_create_table_tests.php b/okay/Sql.ok/_fixtures/migrations/0001_create_table_tests.php new file mode 100644 index 0000000..600e72e --- /dev/null +++ b/okay/Sql.ok/_fixtures/migrations/0001_create_table_tests.php @@ -0,0 +1,19 @@ +table('tests'); + + $table + ->addColumn('name', 'string', ['limit' => 41, 'null' => false]) + ->addColumn('email', 'string', ['limit' => 50, 'null' => false]) + ; + + $table->create(); + } +} diff --git a/okay/Sql.ok/_fixtures/phinx.php b/okay/Sql.ok/_fixtures/phinx.php new file mode 100644 index 0000000..c5b2f50 --- /dev/null +++ b/okay/Sql.ok/_fixtures/phinx.php @@ -0,0 +1,40 @@ + 'phinx', + 'default_database' => 'seeded', + 'logging' => true, + 'version_order' => 'creation', + 'paths' => [ + 'migrations' => __DIR__ . '/migrations', + 'seeds' => __DIR__ . '/seeds', + ], + 'sqlite' => [ + 'user' => '', + 'pass' => '', + 'dir' => '/tmp/pisara-fixtures', + 'which' => [ + 'snapshots' => [ + 'dir' => '/tmp/pisara-snapshots' + ] + ] + ], + 'environments' => [ + 'empty' => [ + 'adapter' => 'sqlite', + 'name' => "empty", + 'migrate' => [ + 'target' => '0001', + 'seeders' => false + ] + ], + 'seeded' => [ + 'adapter' => 'sqlite', + 'name' => "seeded", + 'migrate' => [ + 'seeders' => true + ] + ] + ] +]; diff --git a/okay/Sql.ok/_fixtures/seeds/TestsSeeder.php b/okay/Sql.ok/_fixtures/seeds/TestsSeeder.php new file mode 100644 index 0000000..ea85072 --- /dev/null +++ b/okay/Sql.ok/_fixtures/seeds/TestsSeeder.php @@ -0,0 +1,30 @@ + $faker->userName, + 'email' => $faker->email, + ]; + } + + $this->table('tests')->insert($data)->save(); + } + +} diff --git a/okay/Sql.ok/_initialize.php b/okay/Sql.ok/_initialize.php new file mode 100644 index 0000000..b938ec8 --- /dev/null +++ b/okay/Sql.ok/_initialize.php @@ -0,0 +1,12 @@ +choices() as $choice) { + $READER->choose($choice)->clobber(true); // delete fixture + $READER->choose($choice)->which('snapshots')->create(true); // delete and re-migrate i.e. re-create fixture +} + \ No newline at end of file diff --git a/okay/Sql.ok/_ok.php b/okay/Sql.ok/_ok.php new file mode 100644 index 0000000..6d8628c --- /dev/null +++ b/okay/Sql.ok/_ok.php @@ -0,0 +1,22 @@ +choose('seeded')->which('snapshots')->copyTo($READER->choose('seeded')); + +$PERSISTENCE = new Persistence\Sql( $environment->pdo() ); + diff --git a/persistence/Sql.php b/persistence/Sql.php new file mode 100644 index 0000000..3cd052e --- /dev/null +++ b/persistence/Sql.php @@ -0,0 +1,330 @@ +addRole('Model_AsQuery', $this); + } + + function useAsNew($model) + { + return $model->addRole('Model_NotYetPersisted', $this); + } + + function useAsExisting($model) + { + return $model->addRole('Model_Persisted', $this)->remember(); + } + + function newQuery($aClass = '\Actor\StdClass') + { + $model = new $aClass(); + return $model->addRole('Model_AsQuery', $this); + } + + // Construct + function __construct($pdo, $optionalInfoCache = null) + { + $this->pdo = $pdo; + $this->infoCache = $optionalInfoCache ?? $this; // optional + } + + function withTable($table, $primaryKey = null, $fields = '*') + { + $clone = clone $this; + $clone->table = $table; + $clone->primaryKey = $primaryKey; + + if ($primaryKey && $fields !== '*') $fields = "{$primaryKey},{$fields}"; + $clone->fields = $fields; + + return $clone; + } + + function withFields($fieldsStr) + { + $clone = clone $this; + $clone->fields = $fieldsStr; + return $clone; + } + + function tableFor($modelClass) + { + return $this->table ?? $this->table = strtolower($modelClass::plural); + } + + function primaryKeyFor($modelClass) + { + return $this->primaryKey ?? $this->primaryKey = $this->tableFor($modelClass) . '_id'; + } + + function fetchAll($modelClass = '\Actor\StdClass', $fields = null) + { + return $this->dbSelectAll($modelClass, $fields); + } + + // utils + // + // returns array of objects found (or empty array) +// function dbWhereIn($modelClass, $matchColumn, $array, $fields = '*') +// { +// $table = $this->tableFor($modelClass); +// +// $fields = implode(',', $fields); +// +// $in = str_repeat('?,', count($array) - 1); +// +// return $this->pdo->run("SELECT {$fields} FROM `{$table}` WHERE `{$matchColumn}` IN ({$in}?)", $array)->fetchAllAsObjects($modelClass, [$this]); +// } + + function dbSelectAll($modelClass, $fields = null) + { + $table = $this->tableFor($modelClass); + + $fields = $fields ?? $this->fields; + + $stmt = $this->pdo->run("SELECT {$fields} FROM `{$table}`"); + + return $stmt->asObjects($modelClass, $this)->fetchAll(); + } + + // // Versatile "WHERE IS" query matching multiple column => values. + // returns array of objects found (or empty array) + function dbSelectWhereIs($modelClass, $dict, $fields = null) + { + $table = $this->tableFor($modelClass); + + $fields = $fields ?? $this->fields; + + $is = "(`" . implode('` IS ?) OR (`', array_keys($dict)) . '` IS ?)'; + + $stmt = $this->pdo->run("SELECT {$fields} FROM `{$table}` WHERE {$is}", array_values($dict)); + + return $stmt->asObjects($modelClass, $this)->fetchAll(); + } + + // Versatile "DELETE WHERE IS" query matching multiple column => values. + // returns array of objects found (or empty array) + function dbDeleteWhereIs($modelClass, $dict) + { + $table = $this->tableFor($modelClass); + + $is = "(`" . implode('` IS ?) OR (`', array_keys($dict)) . '` IS ?)'; + + return $this->pdo->run("DELETE FROM `{$table}` WHERE {$is}", array_values($dict)); + } + + // Versatile "INSERT INTO" column => values. + // returns array of objects found (or empty array) + function dbInsertInto($modelClass, $dict) + { + $table = $this->tableFor($modelClass); + + $into = "(`" . implode('`,`', array_keys($dict)) . '`)'; + $values = str_repeat('?,', count($dict) - 1); + + return $this->pdo->run("INSERT INTO `{$table}`{$into} VALUES ({$values}?)", array_values($dict)); + } + + function dbUpdate($modelClass, $dict, $prevDict = []) + { + $table = $this->tableFor($modelClass); + $key = $this->primaryKeyFor($modelClass); + + $is = "`" . $key . "` IS '" . $dict[$key] . "'"; + + $update = array_diff_assoc($dict , $prevDict ); + + if ($update[$key] ?? false) unset($update[$key]); + + $into = "`" . implode('` = ?, `', array_keys($update)) . '` = ?'; + $values = str_repeat('?,', count($update) - 1); + + $this->pdo->run("UPDATE `{$table}` SET {$into} WHERE {$is}", array_values($update)); + + return true; + } + + function CONCAT($list) + { + return $this->pdo->helper->CONCAT($list); + } + + function limitFor($table) + { + $limits = $this->configAt('query_limits') ?? []; + return $limits[$table] ?? $limits['DEFAULT'] ?? 10; + } + + function columnsOf($modelClass) + { + $table = $this->tableFor($modelClass); + + return $this->infoCache->dbInfoAt("{$table}_columns", + function( $k ) use ($table) { + return $this->pdo->columnsOfTable($table); + }); + } + + // fallback when no infoCache provider is supplied + function dbInfoAt($k, $fnOrVal = null) + { + static $cache = []; + if (isset($cache[$k])) return $cache[$k]; + + return (is_callable($fnOrVal)) ? $cache[$k] = $fnOrVal($k) : $cache[$k] = $fnOrVal; + } + + // output + + function dbResultsFrom($stmt, $modelClass, $noData = null) + { + if ($modelClass === 1) { + $results = $stmt->fetchColumn(); + } else if ($modelClass !== null) { + $results = $stmt->asObjects($modelClass, $this)->fetchAll(); + } else { + $results = $modelClass->fetchAll(); + } + + if ($results === false) return $this->respondNoData($noData); + + return $results; + } + } + +} + +/** + * Roles are defined in a sub-namespace of the context as a workaround for the fact that + * PHP doesn't support inner classes. + * + * We use the trait keyword to define roles because the trait keyword is native to PHP (PHP 5.4+). + * In an ideal world it would be better to have a "role" keyword -- think of "trait" as just + * our implementation technique for roles in PHP. + * (This particular implmentation for PHP actually uses a separate class for the role behind the scenes, + * but that programmer needn't be aware of that.) + */ + +namespace Persistence\Sql\Roles { + + // an idea to test + trait AllModels + { + + function myClass() + { + return get_class($this->getDataObject()); + } + } + + trait Model_AsQuery + { + + // Given a partial object with only a few key properties filled in + // query the database + function fetch($fields = null) + { + $modelClass = get_class($this->getDataObject()); + + $lookup = $this->toArray(); + + return $this->context->dbSelectWhereIs($modelClass, $lookup, $fields); + } + + function fetchOne($fields = null) + { + $found = $this->fetch($fields); + + return empty($found) ? null : $found[0]; + } + + function fetchAll($fields = null) + { + $model = $this->getDataObject(); + $modelClass = get_class($model); + + return $this->context->dbSelectAll($modelClass, $fields); + } + + function delete() + { + $model = $this->getDataObject(); + $modelClass = get_class($model); + + $lookup = $this->toArray(); + + return $this->context->dbDeleteWhereIs($modelClass, $lookup); + } + } + + trait Model_NotYetPersisted + { + + function create() + { + $create = $this->toArray(); + + return $this->context->dbInsertInto($this->modelClass(), $create); + } + + function save() + { + return $this->create(); + } + } + + trait Model_Persisted + { + protected $remembered; + + function remember() + { + $this->remembered = $this->toArray(); + } + + function update() + { + $update = $this->toArray(); + + return $this->context->dbUpdate($this->modelClass(), $update, $this->remembered); + } + + function delete() + { + $key = $this->context->primaryKeyFor($this->modelClass()); + $lookup = [$key => $model[$key]]; + + return $this->context->dbDeleteWhereIs($this->modelClass(), $lookup); + } + + function save() + { + return $this->update(); + } + } + +} + \ No newline at end of file diff --git a/src/Actor/.gitplaceholder b/src/Actor/.gitplaceholder new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Actor/.gitplaceholder @@ -0,0 +1 @@ + diff --git a/src/Persistence/NoData.php b/src/Persistence/NoData.php new file mode 100644 index 0000000..89a21fa --- /dev/null +++ b/src/Persistence/NoData.php @@ -0,0 +1,22 @@ +getMessage(); + return $array; + } + + function traceOn($array) { + $array['trace'] = $this->getTrace()[0]; + return $array; + } + +} diff --git a/stages/.gitplaceholder b/stages/.gitplaceholder new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/stages/.gitplaceholder @@ -0,0 +1 @@ +