Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..223588b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/vendor/
+/nbproject/private/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3e86f82
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "spitfire/database",
+ "description": "Database components for spitfire/engine",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "César de la Cal Bretschneider",
+ "email": "cesar@magic3w.com"
+ }
+ ],
+ "autoload": {
+ "psr-4" : {
+ "spitfire\\storage\\database\\" : "./src/"
+ }
+ },
+ "require": {},
+ "require-dev": {
+ "phpunit/phpunit": "^9.4"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..4c8d928
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1975 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "00300841fd3cf62aa76345c79da18f32",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
+ "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-05-29T17:27:14+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.10.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-29T13:22:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.10.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "658f1be311a230e0907f5dfe0213742aff0596de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/658f1be311a230e0907f5dfe0213742aff0596de",
+ "reference": "658f1be311a230e0907f5dfe0213742aff0596de",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "time": "2020-09-26T10:30:38+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "time": "2020-06-27T14:33:11+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0",
+ "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "time": "2020-06-27T14:39:04+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "5.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.3",
+ "webmozart/assert": "^1.9.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "account@ijaap.nl"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "time": "2020-09-03T19:13:55+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "time": "2020-09-17T18:55:26+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.2",
+ "php": "^7.2 || ~8.0, <8.1",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "sebastian/comparator": "^3.0 || ^4.0",
+ "sebastian/recursion-context": "^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^6.0",
+ "phpunit/phpunit": "^8.0 || ^9.0 <9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.11.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "time": "2020-09-29T09:10:42+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "6b20e2055f7c29b56cb3870b3de7cc463d7add41"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6b20e2055f7c29b56cb3870b3de7cc463d7add41",
+ "reference": "6b20e2055f7c29b56cb3870b3de7cc463d7add41",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.10.2",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "*",
+ "ext-xdebug": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-30T10:46:41+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:57:25+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa",
+ "reference": "3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.1",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpspec/prophecy": "^1.12.1",
+ "phpunit/php-code-coverage": "^9.2",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.5",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.3",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^2.3",
+ "sebastian/version": "^3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*",
+ "phpspec/prophecy-phpunit": "^2.0.1"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.4-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ],
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "funding": [
+ {
+ "url": "https://phpunit.de/donate.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-19T09:23:29+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:49:45+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:10:38+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:52:38+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:24:23+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "a90ccbddffa067b51f574dea6eb25d5680839455"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455",
+ "reference": "a90ccbddffa067b51f574dea6eb25d5680839455",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:55:19+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "acf76492a65401babcf5283296fa510782783a7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/acf76492a65401babcf5283296fa510782783a7a",
+ "reference": "acf76492a65401babcf5283296fa510782783a7a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T17:03:56+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:17:30+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2",
+ "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:18:59+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.20-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-10-23T14:02:19+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2020-07-12T23:59:07+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0 || ^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<3.9.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "time": "2020-07-08T17:02:28+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "1.1.0"
+}
diff --git a/nbproject/project.properties b/nbproject/project.properties
new file mode 100644
index 0000000..112ce7b
--- /dev/null
+++ b/nbproject/project.properties
@@ -0,0 +1,7 @@
+include.path=${php.global.include.path}
+php.version=PHP_74
+source.encoding=UTF-8
+src.dir=.
+tags.asp=false
+tags.short=false
+web.root=.
diff --git a/nbproject/project.xml b/nbproject/project.xml
new file mode 100644
index 0000000..86c7a7b
--- /dev/null
+++ b/nbproject/project.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+ <type>org.netbeans.modules.php.project</type>
+ <configuration>
+ <data xmlns="http://www.netbeans.org/ns/php-project/1">
+ <name>database</name>
+ </data>
+ </configuration>
+</project>
diff --git a/src/Aggregate.php b/src/Aggregate.php
new file mode 100644
index 0000000..b19a663
--- /dev/null
+++ b/src/Aggregate.php
@@ -0,0 +1,55 @@
+<?php namespace spitfire\storage\database;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2019 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+class Aggregate
+{
+
+
+ /**
+ *
+ * @var QueryField
+ */
+ private $field;
+
+ /**
+ *
+ * @param QueryField $field
+ * @param string $operation
+ */
+ public function __construct(QueryField $field) {
+ $this->field = $field;
+ }
+
+ public function getField(): QueryField {
+ return $this->field;
+ }
+
+ public function setField(QueryField $field) {
+ $this->field = $field;
+ return $this;
+ }
+
+}
diff --git a/src/AggregateFunction.php b/src/AggregateFunction.php
new file mode 100644
index 0000000..238fb5a
--- /dev/null
+++ b/src/AggregateFunction.php
@@ -0,0 +1,93 @@
+<?php namespace spitfire\storage\database;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2019 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+class AggregateFunction
+{
+
+
+ /**
+ * Indicates that a query is accumulating the results and counting them
+ */
+ const AGGREGATE_COUNT = 'count';
+
+ /**
+ *
+ * @var QueryField
+ */
+ private $field;
+
+ /**
+ *
+ * @var string
+ */
+ private $operation;
+
+ /**
+ * The alias allows the system to later recycle the
+ *
+ * @var string
+ */
+ private $alias;
+
+ /**
+ *
+ * @param QueryField $field
+ * @param string $operation
+ */
+ public function __construct(QueryField $field, $operation) {
+ $this->field = $field;
+ $this->operation = $operation;
+ $this->alias = '__META__' . $field->getField()->getName() . '_' . $operation . rand(0, 10);
+ }
+
+ public function getField(): QueryField {
+ return $this->field;
+ }
+
+ public function getOperation() {
+ return $this->operation;
+ }
+
+ public function getAlias() {
+ return $this->alias;
+ }
+
+ public function setField(QueryField $field) {
+ $this->field = $field;
+ return $this;
+ }
+
+ public function setOperation($operation) {
+ $this->operation = $operation;
+ return $this;
+ }
+
+ public function setAlias($alias) {
+ $this->alias = $alias;
+ return $this;
+ }
+
+}
diff --git a/src/CompositeRestriction.php b/src/CompositeRestriction.php
new file mode 100644
index 0000000..fee0f8f
--- /dev/null
+++ b/src/CompositeRestriction.php
@@ -0,0 +1,143 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\Field as Logical;
+use spitfire\Model;
+use BadMethodCallException;
+
+class CompositeRestriction
+{
+ private $parent;
+ private $field;
+ private $value;
+ private $operator;
+
+ public function __construct(RestrictionGroup$parent, Logical$field = null, $value = null, $operator = Restriction::EQUAL_OPERATOR) {
+
+ if ($value instanceof Model) { $value = $value->getQuery(); }
+ if ($value instanceof Query) { $value->setAliased(true); }
+ else { throw new BadMethodCallException('Composite restriction requires a query / model as value', 1804201334); }
+
+ $this->parent = $parent;
+ $this->field = $field;
+ $this->value = $value;
+ $this->operator = $operator;
+ }
+
+ /**
+ *
+ * @return Query
+ */
+ public function getQuery() {
+ return $this->parent? $this->parent->getQuery() : null;
+ }
+
+ /**
+ *
+ * @return RestrictionGroup
+ */
+ public function getParent() {
+ return $this->parent;
+ }
+
+ public function setQuery(Query$query) {
+ $this->parent = $query;
+ }
+
+ public function setParent(RestrictionGroup$query) {
+ $this->parent = $query;
+ return $this;
+ }
+
+ public function getField() {
+ return $this->field;
+ }
+
+ public function setField(Logical$field) {
+ $this->field = $field;
+ }
+
+ /**
+ *
+ * @return Query
+ */
+ public function getValue() {
+ if ($this->value instanceof Model) { $this->value = $this->value->getQuery(); }
+ return $this->value;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ }
+
+ public function getOperator() {
+ return $this->operator === null? '=' : $this->operator;
+ }
+
+ public function setOperator($operator) {
+ $this->operator = $operator;
+ }
+
+ public function getSubqueries() {
+ $r = array_merge($this->getValue()->getSubqueries(), [$this->getValue()]);
+ return $r;
+ }
+
+ public function replaceQueryTable($old, $new) {
+
+ //TODO: The fact that the composite restriction is not using query tables is off-putting
+ return true;
+
+ }
+
+ public function makeConnector() {
+ $field = $this->getField();
+ $value = $this->getValue();
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
+ $connector = $field->getConnectorQueries($this->getQuery());
+
+ $last = array_pop($connector);
+ $last->setId($this->getValue()->getId());
+
+ if ($field === null || $value === null) {
+ throw new PrivateException('Deprecated: Composite restrictions do not receive null parameters', 2801191504);
+ }
+
+ /**
+ *
+ * @var MysqlPDOQuery The query
+ */
+ $group = $of->restrictionGroupInstance($this->getQuery(), RestrictionGroup::TYPE_AND);
+
+ /**
+ * The system needs to create a copy of the subordinated restrictions
+ * to be able to syntax a proper SQL query.
+ */
+ $group->add($last->toArray());
+
+ /*
+ * Once we looped over the sub restrictions, we can determine whether the
+ * additional group is actually necessary. If it is, we add it to the output
+ */
+ if (!$group->isEmpty()) {
+ $this->getValue()->push($group);
+ }
+
+ return $connector;
+ }
+
+ public function negate() {
+ switch ($this->operator) {
+ case '=':
+ return $this->operator = '<>';
+ case '<>':
+ return $this->operator = '=';
+ case '!=':
+ return $this->operator = '=';
+ }
+ }
+
+ public function __clone() {
+ $this->value = clone $this->value;
+ }
+
+}
diff --git a/src/DB.php b/src/DB.php
new file mode 100644
index 0000000..aba99d8
--- /dev/null
+++ b/src/DB.php
@@ -0,0 +1,185 @@
+<?php namespace spitfire\storage\database;
+
+use BadMethodCallException;
+use spitfire\cache\MemoryCache;
+use spitfire\core\Environment;
+use spitfire\exceptions\PrivateException;
+use spitfire\io\CharsetEncoder;
+use spitfire\storage\database\restrictionmaker\RestrictionMaker;
+
+/**
+ * This class creates a "bridge" beetwen the classes that use it and the actual
+ * driver.
+ *
+ * @author César de la Cal <cesar@magic3w.com>
+ */
+abstract class DB
+{
+
+ private $settings;
+
+ private $tables;
+ private $encoder;
+ private $restrictionMaker;
+
+ /**
+ * Creates an instance of DBInterface. If options are set it will import
+ * them. Otherwise it will try to read them from the current environment.
+ *
+ * @param Settings $settings Parameters passed to the database
+ */
+ public function __construct(Settings$settings) {
+ $this->settings = $settings;
+ $this->tables = new TablePool($this);
+ $this->encoder = new CharsetEncoder(Environment::get('system_encoding'), $settings->getEncoding());
+ $this->restrictionMaker = new RestrictionMaker();
+ }
+
+ /**
+ * The encoder will allow the application to encode / decode database that
+ * is directed towards or comes from the database.
+ *
+ * @return CharsetEncoder
+ */
+ public function getEncoder() {
+ return $this->encoder;
+ }
+
+ /**
+ * Gets the connection settings for this connection.
+ *
+ * @return Settings
+ */
+ public function getSettings() {
+ return $this->settings;
+ }
+
+ /**
+ * Attempts to repair schema inconsistencies. These method is not meant
+ * to be called by the user but aims to provide an endpoint the driver
+ * can use when running into trouble.
+ *
+ * This method does not actually repair broken databases but broken schemas,
+ * if your database is broken or data on it corrupt you need to use the
+ * DBMS specific tools to repair it.
+ *
+ * Repairs the list of tables/models <b>currently</b> loaded into the db.
+ * If a model hasn't been accessed during execution it won't be listed
+ * here.
+ *
+ * Please note, that this function is used only for maintenance and repair
+ * works on tables. Meaning that it is not relevant if <b>all</b> tables
+ * were imported.
+ */
+ public function repair() {
+ $tables = $this->tables->getCache()->getAll();
+ foreach ($tables as $table) {
+ $table->getLayout()->repair();
+ }
+ }
+
+ /**
+ * Returns a table adapter for the database table with said name to allow
+ * querying and data-manipulation..
+ *
+ * @param string|Schema $tablename Name of the table that should be used.
+ * If you pass a model to this function it will automatically
+ * read the name from the model and use it to find the
+ * table.
+ *
+ * @throws PrivateException If the table could not be found
+ * @return Table The database table adapter
+ */
+ public function table($tablename) {
+
+ #If the parameter is a Model, we get it's name
+ if ($tablename instanceof Schema) {
+ return $this->tables->set($tablename->getName(), new Table($this, $tablename));
+ }
+
+ #We just tested if it's a Schema, let's see if it's a string
+ if (!is_string($tablename)) {
+ throw new BadMethodCallException('DB::table requires Schema or string as argument');
+ }
+
+ #Check if the table can be found in the table cache
+ return $this->tables->get($tablename);
+ }
+
+ /**
+ * Returns our table cache. This allows an application that uses Spitfire (or
+ * it's own core) to check whether a table is already cached or inject it's
+ * own tables.
+ *
+ * @return TablePool
+ */
+ public function getTableCache() {
+ return $this->tables;
+ }
+
+ /**
+ *
+ * @return RestrictionMaker
+ */
+ public function getRestrictionMaker() {
+ return $this->restrictionMaker;
+ }
+
+ /**
+ * Allows short-hand access to tables by using: $db->tablename
+ *
+ * @param string $table Name of the table
+ * @return Table
+ */
+ public function __get($table) {
+ #Otherwise we try to get the table with this name
+ return $this->table($table);
+ }
+
+ /**
+ * Returns the handle used by the system to connect to the database.
+ * Depending on the driver this can be any type of content. It should
+ * only be used by applications with special needs.
+ *
+ * @abstract
+ * @return mixed The connector used by the system to communicate with
+ * the database server. The data-type of the return value depends on
+ * the driver used by the system.
+ */
+ abstract public function getConnection();
+
+ /**
+ * Allows the application to create the database needed to store the tables
+ * and therefore data for the application. Some DBMS like SQLite won't support
+ * multiple databases - so this may not do anything.
+ *
+ * @abstract
+ * @return bool Returns whether the operation could be completed successfully
+ */
+ abstract public function create();
+
+ /**
+ * Destroys the database and all of it's contents. Drivers may not allow
+ * this method to be called unless they're being operated in debug mode or
+ * a similar mode.
+ *
+ * @abstract
+ * @throws PrivateException If the driver rejected the operation
+ * @return bool Whether the operation could be completed
+ */
+ abstract public function destroy();
+
+ /**
+ * In modern spitfire drivers, all the object creation for a database is handled
+ * by the object factories. This factories allow the system to create any object
+ * they need: Queries, Tables, Fields...
+ *
+ * This removes the need to have some driver specific objects just for the
+ * sake of providing a certain type. This way all SQL drivers can share some
+ * standard components while replacing the ones they specifically need.
+ *
+ * @return ObjectFactoryInterface
+ */
+ abstract public function getObjectFactory();
+
+}
diff --git a/src/Field.php b/src/Field.php
new file mode 100644
index 0000000..c79537c
--- /dev/null
+++ b/src/Field.php
@@ -0,0 +1,133 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\Field as LogicalField;
+
+/**
+ * The 'database field' class is an adapter used to connect logical fields
+ * (advanced fields that can contain complex data) to simplified versions
+ * that common DBMSs can use to store this data.
+ *
+ * This class should be extended by each driver to allow it to use them in an
+ * efficient manner for them.
+ */
+abstract class Field
+{
+ /**
+ * Contains a reference to the logical field that contains information
+ * relevant to this field.
+ *
+ * @var \spitfire\model\Field
+ */
+ private $logical;
+
+ /**
+ * Provides a name that the DBMS should use to name this field. Usually
+ * this will be exactly the same as for the logical field, except for
+ * fields that reference others.
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * This allows the field to provide information about a field it refers
+ * to on another table on the same DBMS. This helps connecting the fields
+ * and generating links on the DBMS that allow the engine to optimize
+ * queries that Spitfire generates.
+ *
+ * @var \spitfire\storage\database\Field
+ */
+ private $referenced;
+
+ /**
+ * Creates a new Database field. This fields provide information about
+ * how the DBMS should hadle one of Spitfire's Model Fields. The Model
+ * Fields, also referred to as Logical ones can contain data that
+ * requires several DBFields to store, this class creates an adapter
+ * to easily handle the different objects.
+ *
+ * @param \spitfire\model\Field $logical
+ * @param string $name
+ * @param \spitfire\storage\database\Field $references
+ */
+ public function __construct(LogicalField$logical, $name, Field$references = null) {
+ $this->logical = $logical;
+ $this->name = $name;
+ $this->referenced = $references;
+ }
+
+ /**
+ * Returns the fully qualified name for this column on the DBMS. Fields
+ * referring to others will return an already prefixed version of them
+ * like 'field_remote'.
+ *
+ * In order to obtain the field name you can request it from the logical
+ * field. And to obtain the remote name you can request it from the
+ * referenced field.
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setName($name) {
+ $this->name = $name;
+ }
+
+ /**
+ * Returns the field this one is referencing to. This allows the DBMS to
+ * establish relations. In case this field does not reference any other
+ * it'll return null.
+ *
+ * @return \spitfire\storage\database\Field|null
+ */
+ public function getReferencedField() {
+ return $this->referenced;
+ }
+
+ /**
+ * Defines which field is referenced by this one, this allows the DBMS to set
+ * relations from one field to another and enforce referential integrity.
+ *
+ * @param \spitfire\storage\database\Field $referenced
+ */
+ public function setReferencedField(Field$referenced) {
+ $this->referenced = $referenced;
+ }
+
+
+ /**
+ * Returns the logical field that contains this Physical field. The
+ * logical field contains relevant data about what data it contains
+ * and how it relates.
+ *
+ * @return \spitfire\model\Field
+ */
+ public function getLogicalField() {
+ return $this->logical;
+ }
+
+ /**
+ * Defines which logical field this belongs to. The logical field is the one
+ * in charge to provide data about the field and the data it contains.
+ *
+ * @param LogicalField $logical
+ */
+ public function setLogicalField(LogicalField$logical) {
+ $this->logical = $logical;
+ }
+
+ /**
+ * This function returns the table this field belongs to. This function
+ * is a convenience shortcut that allows retrieving the Table without
+ * needing to read all the intermediate layers that compound the logical
+ * template of the Table.
+ *
+ * @return \spitfire\storage\database\Table
+ */
+ public function getTable() {
+ return $this->getLogicalField()->getModel()->getTable();
+ }
+
+}
diff --git a/src/ForeignKeyInterface.php b/src/ForeignKeyInterface.php
new file mode 100644
index 0000000..b4c27bb
--- /dev/null
+++ b/src/ForeignKeyInterface.php
@@ -0,0 +1,40 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\core\Collection;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+interface ForeignKeyInterface extends IndexInterface
+{
+
+ /**
+ * Returns a collection of fields that the index is referencing. This allows
+ * the application to properly define indexes that do only exist for the
+ * purpose of linking two tables.
+ *
+ * @return Collection of FieldInterface
+ */
+ function getReferenced() : Collection;
+}
\ No newline at end of file
diff --git a/src/IndexInterface.php b/src/IndexInterface.php
new file mode 100644
index 0000000..1ea0424
--- /dev/null
+++ b/src/IndexInterface.php
@@ -0,0 +1,75 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\core\Collection;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * This interface allows the database driver to provide the system with indexes
+ * it can understand. These indexes will be fed back to the driver in the event
+ * the system detects a missing table. Allowing the driver to assemble the table
+ * properly.
+ */
+interface IndexInterface
+{
+
+ /**
+ * Returns an array of fields, please note that your driver should respect
+ * the order on these. The order of the fields may affect query performance
+ * heavily on a relational model.
+ *
+ * @return Collection <Field>
+ */
+ function getFields();
+
+ /**
+ * Returns a name for the index. In most DBMS this is optional, but allows
+ * for better understanding of the schema.
+ *
+ * @return string
+ */
+ function getName() : string;
+
+ /**
+ * Indicates whether this is a unique index. Therefore requesting the DBMS to
+ * enforce no-duplicates on the index.
+ *
+ * A driver requesting this value should always OR this value with isPrimary()
+ * like $index->isOptional() || $index->isPrimary() to know whether a index
+ * is unique.
+ *
+ * @see IndexInterface::isPrimary()
+ * @return bool
+ */
+ function isUnique() : bool;
+
+ /**
+ * Indicates whether this index is primary. If your index returns this value
+ * as true, the isUnique() value will be overriden by the system internally.
+ *
+ * @return bool
+ */
+ function isPrimary() : bool;
+}
\ No newline at end of file
diff --git a/src/LayoutInterface.php b/src/LayoutInterface.php
new file mode 100644
index 0000000..40e4e90
--- /dev/null
+++ b/src/LayoutInterface.php
@@ -0,0 +1,77 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\core\Collection;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * The layout is basically a list of columns + indexes that makes up the schema
+ * of a relation in a relational database.
+ *
+ * A driver can implement this interface to provide common operations on it's
+ * tables for spitfire to run.
+ */
+interface LayoutInterface
+{
+
+ /**
+ * Returns the name the DBMS should use to name this table. The implementing
+ * class should respect user configuration including db_table_prefix
+ *
+ * @return string
+ */
+ function getTableName() : string;
+
+ /**
+ *
+ * @param string $name
+ * @return Field
+ */
+ function getField($name) : Field;
+
+ /**
+ *
+ * @return Field[] The columns in this database table
+ */
+ function getFields();
+
+ /**
+ * This method needs to get the lost of indexes from the logical Schema and
+ * convert them to physical indexes for the DBMS to manage.
+ *
+ * @return Collection (IndexInterface) The indexes in this layout
+ */
+ function getIndexes();
+
+
+ /**
+ * Creates a table on the DBMS that is capable of holding the Model's data
+ * appropriately. This will try to normalize the data as far as possible to
+ * create consistent databases.
+ */
+ function create();
+ function repair();
+ function destroy();
+}
\ No newline at end of file
diff --git a/src/ObjectFactoryInterface.php b/src/ObjectFactoryInterface.php
new file mode 100644
index 0000000..4268791
--- /dev/null
+++ b/src/ObjectFactoryInterface.php
@@ -0,0 +1,158 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\Field as LogicalField;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * The database object factory is a class that allows a driver to provide SF's
+ * ORM with all the required bits and pieces to operate. Usually a driver needs
+ * to provide it's own Table, Query, Field... objects that implement / extend
+ * the behavior required for the ORM to work.
+ *
+ * Historically, a query would provide only the pieces it needed, as well as the
+ * table would. But for consistency, and to avoid generating classes that only
+ * need to extend in order to provide factories we're merging those behaviors
+ * in this single factory.
+ */
+interface ObjectFactoryInterface
+{
+
+ /**
+ * Creates a relation. These wrap the typical record operations on a table
+ * into a separate layer.
+ *
+ * @param Table $table
+ *
+ * @return Relation
+ */
+ function makeRelation(Table$table);
+
+ /**
+ * Creates a table layout to generate an appropriate schema for the DBMS to
+ * store the data.
+ *
+ * @param Table $table
+ *
+ * @return LayoutInterface The layout for the table
+ */
+ function makeLayout(Table$table);
+
+ /**
+ * Creates a new On The Fly Schema. These allow the system to interact with a
+ * database that was not modeled after Spitfire's models or that was not
+ * reverse engineered previously.
+ *
+ * @param string $modelname
+ *
+ * @return Table Instance of the table class the driver wants the system to use
+ * @todo Rename to generateSchema
+ */
+ function getOTFSchema(DB$db, $modelname);
+
+ /**
+ * Creates an instance of the Database field compatible with the current
+ * DBMS. As opposed to the Logical fields, physical fields do not accept
+ * complex values, just basic types that any DBMS can handle.
+ *
+ * @param Field $field
+ * @param string $name
+ * @param Field $references
+ *
+ * @return Field Field
+ * @todo Rename to makeField
+ */
+ function getFieldInstance(LogicalField$field, $name, Field$references = null);
+
+ /**
+ * Creates a new restriction. This combines a query with a field and a value
+ * which allows to create the queries that we need to construct in order to
+ * retrieve data.
+ *
+ * @param string $query
+ * @param QueryField $field
+ * @param mixed $value
+ * @param string|null $operator
+ *
+ * @return Restriction|CompositeRestriction
+ * @todo Rename to makeRestriction
+ */
+ function restrictionInstance($query, QueryField$field, $value, $operator = null);
+
+ /**
+ *
+ * @todo This is supposed to take a RestrictionGroup
+ * @param RestrictionGroup $parent
+ * @param LogicalField $field
+ * @param mixed $value
+ * @param string $operator
+ */
+ function restrictionCompositeInstance(RestrictionGroup$parent, LogicalField$field = null, $value = null, $operator = null);
+
+ /**
+ * Creates a restriction group. This allows to associate several restrictions
+ * with each other to create more complicated queries when writing.
+ *
+ * @param RestrictionGroup $parent
+ * @param int $type
+ * @return RestrictionGroup A restriction group
+ */
+ function restrictionGroupInstance(RestrictionGroup$parent = null, $type = RestrictionGroup::TYPE_OR);
+
+ /**
+ * Creates a new query. A query is created with a table to provide information
+ * where the data should be retrieved some and some information on the fields
+ * that we want it to provide.
+ *
+ * @param Table|Relation $table
+ *
+ * @return Query
+ * @todo Rename to makeQuery
+ */
+ function queryInstance($table);
+
+
+ /**
+ * These objects connect a field with a query, providing an aliased name for
+ * the field when necessary.
+ *
+ * @todo The second parameter should only accept physical and not logical fields
+ *
+ * @param QueryTable $queryTable
+ * @param Field|QueryField $field
+ * @return QueryField
+ */
+ function queryFieldInstance(QueryTable$queryTable, $field);
+
+
+ /**
+ * These objects connect a field with a query, providing an aliased name for
+ * the field when necessary.
+ *
+ * @param QueryTable|Table $table
+ * @return QueryTable
+ */
+ function queryTableInstance($table);
+}
diff --git a/src/Query.php b/src/Query.php
new file mode 100644
index 0000000..dec3a56
--- /dev/null
+++ b/src/Query.php
@@ -0,0 +1,418 @@
+<?php namespace spitfire\storage\database;
+
+use Exception;
+use spitfire\exceptions\PrivateException;
+use spitfire\exceptions\PublicException;
+use spitfire\Model;
+use spitfire\model\Field as LogicalField;
+
+abstract class Query extends RestrictionGroup
+{
+ /**
+ * The result for the query. Currently, this is attached to the query - this
+ * means that whenever the query is "re-executed" the result is overwritten
+ * and could potentially damage the resultset.
+ *
+ * This would require a significant change in the API, since it requires the
+ * app to not loop fetch() calls over the query but actually retrieve the
+ * result element and loop over that.
+ *
+ * @todo This should be removed in favor of an actual collector for the results
+ * @deprecated since version 0.1-dev 20170414
+ * @var \spitfire\storage\database\ResultSetInterface|null
+ */
+ protected $result;
+
+ /**
+ * The table this query is retrieving data from. This table is wrapped inside
+ * a QueryTable object to ensure that the table can refer back to the query
+ * when needed.
+ *
+ * @var QueryTable
+ */
+ protected $table;
+
+ /**
+ *
+ * @todo We should introduce a class that allows these queries to sort by multiple,
+ * and even layered (as in, in other queries) columns.
+ * @var string
+ */
+ protected $order;
+
+ /**
+ * This contains an array of aggregation functions that are executed with the
+ * query to provide metadata on the query.
+ *
+ * @var AggregateFunction[]
+ */
+ protected $calculated;
+
+ /**
+ *
+ * @var Aggregate[]
+ */
+ protected $aggregate = null;
+
+ /** @param Table $table */
+ public function __construct($table) {
+ $this->table = $table->getDb()->getObjectFactory()->queryTableInstance($table);
+
+ #Initialize the parent
+ parent::__construct(null, Array());
+ $this->setType(RestrictionGroup::TYPE_AND);
+ }
+
+ /**
+ *
+ * @param string $fieldname
+ * @param string $value
+ * @param string $operator
+ * @deprecated since version 0.1-dev 20170414
+ * @return Query
+ */
+ public function addRestriction($fieldname, $value, $operator = '=') {
+ $this->result = null;
+ return parent::addRestriction($fieldname, $value, $operator);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
+ * @param boolean $aliased
+ */
+ public function setAliased($aliased) {
+ $this->table->setAliased($aliased);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
+ * @return boolean
+ */
+ public function getAliased() {
+ return $this->table->isAliased();
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
+ * @return int
+ */
+ public function getId() {
+ return $this->table->getId();
+ }
+
+ /**
+ *
+ * @param int $id
+ * @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
+ * @return \spitfire\storage\database\Query
+ */
+ public function setId($id) {
+ $this->table->setId($id);
+ return $this;
+ }
+
+ /**
+ * Since a query is the top Level of any group we can no longer climb up the
+ * ladder.
+ *
+ * @throws PrivateException
+ */
+ public function endGroup() {
+ throw new PrivateException('Called endGroup on a query', 1604031547);
+ }
+
+ public function getQuery() {
+ return $this;
+ }
+
+ /**
+ * Sets the amount of results returned by the query.
+ *
+ * @deprecated since version 0.1-dev 20180509
+ * @remove 20180911
+ * @param int $amt
+ *
+ * @return self
+ */
+ public function setResultsPerPage($amt) {
+ trigger_error('Deprecated Query::setResultsPerPage() invoked', E_USER_DEPRECATED);
+ throw new PrivateException('Pagination has been moved. Please refer to the documentation', 1805111238);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20180509
+ * @remove 20180911
+ * @return int The amount of results the query returns when executed.
+ */
+ public function getResultsPerPage() {
+ return false;
+ }
+
+ /**
+ * @deprecated since version 0.1-dev 20170414
+ * @param int $page The page of results currently displayed.
+ * @removed 20180511
+ * @return boolean Returns if the page se is valid.
+ */
+ public function setPage ($page) {
+ trigger_error('Deprecated Query::setPage() invoked', E_USER_DEPRECATED);
+ throw new PrivateException('Pagination has been moved. Please refer to the documentation', 1805111238);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20170414
+ * @remove 20180911
+ * @return int
+ */
+ public function getPage() {
+ trigger_error('Deprecated Query::getPage() invoked', E_USER_DEPRECATED);
+ return false;
+ }
+
+ //@TODO: Add a decent way to sorting fields that doesn't resort to this awful thing.
+ public function setOrder ($field, $mode) {
+
+ if ($field instanceof AggregateFunction || $field instanceof Field) {
+ $this->order['field'] = $field;
+ }
+
+ elseif (is_string($field)) {
+ $this->order['field'] = $this->table->getTable()->getLayout()->getField($field);
+ }
+
+ else {
+ $physical = $this->table->getTable()->getModel()->getField($field)->getPhysical();
+ $this->order['field'] = reset($physical);
+ }
+
+ $this->order['mode'] = $mode;
+ return $this;
+ }
+
+ /**
+ * Returns a record from a database that matches the query we sent.
+ *
+ * @deprecated since version 0.1-dev 20170414
+ * @return Model
+ */
+ public function fetch() {
+ if (!$this->result) { $this->query(); }
+
+ $data = $this->result->fetch();
+ return $data;
+ }
+
+ /**
+ * This method returns a single record from a query, allowing your application
+ * to quickly query the database for a record it needs.
+ *
+ * The onEmpty parameter allows you to inject a callback in the event the query
+ * returns no value. You can provide a callable or an Exception (which will
+ * be thrown), reducing the amount of if/else in your controllers.
+ *
+ * The code
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first();
+ * if (!$user) { throw new PublicException('No user found', 404); }
+ * </code>
+ *
+ * Can therefore be condensed into:
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first(new PublicException('No user found', 404));
+ * </code>
+ *
+ * If you wish to further condense this, you can just provide onEmpty as `true`
+ * which will then cause the system to raise a `PublicException` with a standard
+ * 'No %tablename found' and a 404 code. Causing this code to boil down to:
+ *
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first(true);
+ * </code>
+ *
+ * While this seems more unwieldy at first, the code gains a lot of clarity
+ * when written like this
+ *
+ * @param callable|\Exception|true|null $onEmpty
+ * @return Model|null
+ */
+ public function first($onEmpty = null) {
+ $res = $this->execute(null, 0, 1)->fetch();
+
+ if (!$res && $onEmpty) {
+ if ($onEmpty instanceof \Exception) { throw $onEmpty;}
+ elseif(is_callable($onEmpty)) { return $onEmpty(); }
+ elseif($onEmpty === true) { throw new PublicException(sprintf('No %s found', $this->getTable()->getSchema()->getName()), 404); }
+ }
+
+ return $res;
+ }
+
+ /**
+ * This method returns a finite amount of items matching the parameters from
+ * the database. This method always returns a collection, even if the result
+ * is empty (no records matched the query)
+ *
+ * @param int $skip
+ * @param int $amt
+ * @return \spitfire\core\Collection
+ */
+ public function range($skip = 0, $amt = 1) {
+ if ($skip < 0 || $amt < 1) {
+ throw new \InvalidArgumentException('Query received invalid arguments', 1805091416);
+ }
+
+ return $this->execute(null, $skip, $amt)->fetchAll();
+ }
+
+ /**
+ *
+ * @return \spitfire\core\Collection
+ */
+ public function all() {
+ return $this->execute()->fetchAll();
+ }
+
+ /**
+ * Returns all the records that the query matched. This method wraps the records
+ * inside a collection object to make them easier to access.
+ *
+ * @deprecated since version 0.1-dev 20180509
+ * @return \spitfire\core\Collection[]
+ */
+ public function fetchAll() {
+ if (!$this->result) { $this->query(); }
+ return new \spitfire\core\Collection($this->result->fetchAll());
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20180509 We do no longer provide the option to not return the result
+ * @param mixed[] $fields
+ * @param bool $returnresult
+ * @return type
+ */
+ protected function query($fields = null, $returnresult = false) {
+ $result = $this->execute($fields);
+
+ if ($returnresult) { return $result; }
+ else { return $this->result = $result; }
+ }
+
+ /**
+ * Deletes the records matching this query. This will not retrieve the data and
+ * therefore is more efficient than fetching and later deleting.
+ *
+ * @todo Currently does not support deleting of complex queries.
+ * @return int Number of affected records
+ */
+ public abstract function delete();
+
+ /**
+ * Counts the number of records a query would return. If there is a grouping
+ * defined it will count the number of records each group would return.
+ *
+ * @todo This method's behavior is extremely inconsistent
+ * @return int
+ */
+ public function count() {
+ //This is a temporary fix that will only count distinct values in complex
+ //queries.
+ $query = $this->query(Array('COUNT(DISTINCT ' . $this->table->getTable()->getPrimaryKey()->getFields()->join(', ') . ')'), true)->fetchArray();
+ $count = reset($query);
+ return (int)$count;
+
+ }
+
+ /**
+ * Defines a column or array of columns the system will be using to group
+ * data when generating aggregates.
+ *
+ * @todo When adding aggregation, the system should automatically use the aggregation for extraction
+ * @todo Currently the system only supports grouping and not aggregation, this is a bit of a strange situation that needs resolution
+ *
+ * @param LogicalField|FieLogicalFieldld[]|null $column
+ * @return Query Description
+ */
+ public function aggregateBy($column) {
+ if (is_array($column)) { $this->aggregate = $column; }
+ elseif($column === null) { $this->aggregate = null; }
+ else { $this->aggregate = Array($column); }
+
+ return $this;
+ }
+
+ public function addCalculatedValue (AggregateFunction$fn) {
+ $this->calculated[] = $fn;
+ }
+
+
+ /**
+ * Creates the execution plan for this query. This is an array of queries that
+ * aid relational DBMSs' drivers when generating SQL for the database.
+ *
+ * This basically generate the connecting queries between the tables and injects
+ * your restrictions in between so the system egenrates logical routes that
+ * will be understood by the relational DB.
+ *
+ * @deprecated since version 0.1-dev 20180510
+ * @todo Move somewhere else. This only pertains to relational DBMS systems
+ * @return Query[]
+ */
+ public function makeExecutionPlan() {
+ $_ret = $this->getPhysicalSubqueries();
+ array_push($_ret, $this);
+ return $_ret;
+ }
+
+ public function getOrder() {
+ return $this->order;
+ }
+
+ /**
+ * Returns the current 'query table'. This is an object that allows the query
+ * to alias it's table if needed.
+ *
+ * @return QueryTable
+ */
+ public function getQueryTable() {
+ return $this->table;
+ }
+
+ public function cloneQueryTable() {
+ $table = clone $this->table;
+ $table->newId();
+
+ $this->replaceQueryTable($this->table, $table);
+
+ $this->table = $table;
+ return $this->table;
+ }
+
+ /**
+ * Returns the actual table this query is searching on.
+ *
+ * @return Table
+ */
+ public function getTable() {
+ return $this->table->getTable();
+ }
+
+ public function __toString() {
+ return $this->getTable()->getLayout()->getTableName() . implode(',', $this->toArray());
+ }
+
+ /**
+ *
+ * @return ResultSetInterface
+ */
+ public abstract function execute($fields = null, $offset = null, $max = null);
+}
diff --git a/src/QueryField.php b/src/QueryField.php
new file mode 100644
index 0000000..134edfa
--- /dev/null
+++ b/src/QueryField.php
@@ -0,0 +1,109 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\Field as Logical;
+
+/**
+ * The query field object is a component that allows a Query to wrap a field and
+ * connect it to itself. This is important for the DBA since it allows the app
+ * to establish connections between the different queries when assembling SQL
+ * or similar.
+ *
+ * When a query is connected to a field, you may use this to establish relationships
+ * and create complex queries that can properly be joined.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ * @abstract
+ */
+abstract class QueryField
+{
+ /**
+ * The actual database field. Note that this field is
+ *
+ * @var Logical
+ */
+ private $field;
+
+ /**
+ *
+ * @var QueryTable
+ */
+ private $table;
+
+ public function __construct(QueryTable$table, $field) {
+ $this->table = $table;
+ $this->field = $field;
+ }
+
+ /**
+ * Returns the parent Table for this field.
+ *
+ * @return QueryTable
+ */
+ public function getQueryTable() : QueryTable {
+ return $this->table;
+ }
+
+ public function setQueryTable(QueryTable $table) {
+ $this->table = $table;
+ return $this;
+ }
+
+ /**
+ * Returns the source field for this object.
+ *
+ * @return Logical|Field
+ */
+ public function getField() {
+ return $this->field;
+ }
+
+ /**
+ *
+ *
+ * @return bool
+ */
+ public function isLogical() : bool {
+ return $this->field instanceof Logical;
+ }
+
+ /**
+ * Returns an array of fields that compose the physical components of the
+ * field. This method automatically converts the fields to QueryField so they
+ * can be used again.
+ *
+ * @return Field[]
+ */
+ public function getPhysical() : array {
+ /*
+ * Get the object factory for the current DB connection. It is then used
+ * to create physical copies of logical fields.
+ */
+ $of = $this->table->getTable()->getDb()->getObjectFactory();
+
+ if ($this->isLogical()) {
+ $fields = $this->field->getPhysical();
+
+ foreach ($fields as &$field) {
+ $field = $of->queryFieldInstance($this->table, $field);
+ }
+
+ return $fields;
+ }
+
+ return [$of->queryFieldInstance($this->table, $this->field)];
+ }
+
+ /**
+ * Many drivers use this objects to generate "object identifiers", strings that
+ * indicate what field in which table is being adressed. So we're forcing driver
+ * vendors to implement the __toString method to achieve the most consistent
+ * result possible.
+ *
+ * This may not be the case for your driver. In this event, just return a string
+ * that may be used for debugging and create an additional method for your
+ * driver.
+ *
+ * @return string
+ */
+ abstract public function __toString();
+}
diff --git a/src/QueryTable.php b/src/QueryTable.php
new file mode 100644
index 0000000..0e2eb66
--- /dev/null
+++ b/src/QueryTable.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace spitfire\storage\database;
+
+abstract class QueryTable
+{
+ private $table;
+
+ /**
+ * The following variables manage the aliasing system inside spitfire. To avoid
+ * having different tables with the same name in them, Spitfire uses aliases
+ * for the tables. These aliases are automatically generated by adding a unique
+ * number to the table's name.
+ *
+ * The counter is in charge of making sure that every table is uniquely named,
+ * every time a new query table is created the current value is assigned and
+ * incremented.
+ *
+ * @var int
+ */
+ private static $counter = 1;
+ private $id;
+ private $aliased = false;
+
+ public function __construct(Table$table) {
+ #In case this table is aliased, the unique alias will be generated using this.
+ $this->id = self::$counter++;
+
+ $this->table = $table;
+ }
+
+ public function getId() {
+ return $this->id;
+ }
+
+ public function setId($id) {
+ $this->id = $id;
+ }
+
+ public function newId() {
+ $this->id = self::$counter++;
+ }
+
+ public function setAliased($aliased) {
+ $this->aliased = $aliased;
+ }
+
+ public function isAliased() {
+ return $this->aliased;
+ }
+
+ public function getAlias() {
+ /*
+ * Get the name for the table. We use it to provide a consistent naming
+ * system that makes it easier for debugging.
+ */
+ $name = $this->table->getLayout()->getTablename();
+
+ return $this->aliased? sprintf('%s_%s', $name, $this->id) : $name;
+ }
+
+ public function getField($name) {
+ $of = $this->table->getDb()->getObjectFactory();
+ return $of->queryFieldInstance($this, $this->table->getLayout()->getField($name));
+ }
+
+ public function getFields() {
+ $of = $this->table->getDb()->getObjectFactory();
+ $fields = $this->table->getLayout()->getFields();
+
+ foreach ($fields as &$field) {
+ $field = $of->queryFieldInstance($this, $field);
+ }
+
+ return $fields;
+ }
+
+ /**
+ *
+ * @return \spitfire\storage\database\Table
+ */
+ public function getTable() {
+ return $this->table;
+ }
+
+ abstract public function definition();
+ abstract public function __toString();
+}
\ No newline at end of file
diff --git a/src/Relation.php b/src/Relation.php
new file mode 100644
index 0000000..cfb3a89
--- /dev/null
+++ b/src/Relation.php
@@ -0,0 +1,177 @@
+<?php namespace spitfire\storage\database;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+use spitfire\exceptions\PrivateException;
+use spitfire\Model;
+
+/**
+ * When we think about a table in a common DBMS we think of the combination of
+ * a table schema, which defines the fields and what data they can contain, and
+ * a relation of records which can be used to maintain your data.
+ *
+ * This class represents the collection, it will provide the mechanisms to CRUD
+ * records and also may provide caching for the database.
+ *
+ * @todo Relations should also provide a proper caching mechanism
+ * @todo Move "below" a table.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+abstract class Relation
+{
+
+ /**
+ * Contains the information about the schema used to construct the database
+ * table and methods for table manipulation.
+ *
+ * Usually when we speak of a table in a RDBMS we talk about the combination
+ * of schema + data which assembles the table. The spitfire table is the
+ * schema part and the collection the data.
+ *
+ * @var Table
+ */
+ private $table;
+
+ /**
+ * Creates the collection. We need the table as it provides the infrastructure
+ * for the collection to work.
+ *
+ * @param Table $table
+ */
+ public function __construct(Table$table) {
+ $this->table = $table;
+ }
+
+ /**
+ * Returns the table this collection uses. The table is the object providing
+ * schema, and table operations on the RDBMS.
+ *
+ * @return Table
+ */
+ public function getTable() {
+ return $this->table;
+ }
+
+ public function getDb() {
+ return $this->table->getDb();
+ }
+
+ /**
+ * Returns the bean this model uses to generate Forms to feed itself with data
+ * the returned value normally is a class that inherits from CoffeeBean.
+ *
+ * @param string|null $name
+ *
+ * @return \CoffeeBean
+ */
+ public function getBean($name = null) {
+
+ if (!$name) { $beanName = $this->getTable()->getSchema()->getName() . 'Bean'; }
+ else { $beanName = $name . 'Bean'; }
+
+ $bean = new $beanName($this->getTable());
+
+ return $bean;
+ }
+
+ /**
+ * Creates a new record in this table
+ *
+ * @param array $data
+ *
+ * @return Model Record for the selected table
+ */
+ public function newRecord($data = []) {
+ $classname = $this->table->getModel()->getName() . 'Model';
+
+ if (class_exists($classname)) { return new $classname($this->getTable(), $data); }
+ else { return new \spitfire\model\OTFModel($this->getTable(), $data); }
+ }
+
+ /**
+ * Increments a value on high read/write environments. Using update can
+ * cause data to be corrupted. Increment requires the data to be in sync
+ * aka. stored to database.
+ *
+ * @param Model $record
+ * @param string $key
+ * @param int|float $diff
+ *
+ * @throws PrivateException
+ */
+ public abstract function increment(Model$record, $key, $diff = 1);
+
+ /**
+ * Creates a simple query with a simple restriction applied to it. This
+ * is especially useful for id related queries.
+ *
+ * @param string $field
+ * @param string $value
+ * @param string|null $operator
+ * @return Query
+ */
+ public function get($field, $value, $operator = null) {
+ #Create the query
+ $table = $this->getTable();
+ $query = $table->getDb()->getObjectFactory()->queryInstance($table);
+ $query->addRestriction($field, $value, $operator);
+ #Return it
+ return $query;
+ }
+
+ /**
+ * Creates an empty query that would return all data. This is a syntax
+ * friendliness oriented method as it does exactly the same as startQuery
+ *
+ * @return Query
+ */
+ public function getAll() {
+ $table = $this->getTable();
+ $query = $table->getDb()->getObjectFactory()->queryInstance($table);
+ return $query;
+ }
+
+ public abstract function delete(Model$record);
+ public abstract function insert(Model$record);
+ public abstract function update(Model$record);
+
+ /**
+ * While development is ongoing, we will try to recover from errors due to the
+ * separation between Tables and Collections by sending all calls that were
+ * intended for a table to the table itself
+ *
+ * @todo Remove
+ *
+ * @param string $name
+ * @param mixed $arguments
+ *
+ * @return mixed
+ */
+ public function __call($name, $arguments) {
+ #Pass on
+ trigger_error('Called Relation::__call. This should not happen. Missing argument: ' . $name, E_USER_DEPRECATED);
+ return call_user_func_array(Array($this->getDb(), $name), $arguments);
+ }
+}
diff --git a/src/Restriction.php b/src/Restriction.php
new file mode 100644
index 0000000..9f9241b
--- /dev/null
+++ b/src/Restriction.php
@@ -0,0 +1,155 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\exceptions\PrivateException;
+
+/**
+ * A restriction indicates a condition a record in a database's relation must
+ * satisfy to be returned by a database query.
+ *
+ * Restrictions can be either simple (like these) or composite. These simple ones
+ * can only contain basic data-types like integers, floats, strings or enums as
+ * their value.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+abstract class Restriction
+{
+ /**
+ *
+ * @var RestrictionGroup
+ */
+ private $parent;
+
+ /**
+ *
+ * @var QueryField
+ */
+ private $field;
+ private $value;
+ private $operator;
+
+ const LIKE_OPERATOR = 'LIKE';
+ const EQUAL_OPERATOR = '=';
+
+ public function __construct($parent, $field, $value, $operator = '=') {
+ if (is_null($operator)) {
+ $operator = self::EQUAL_OPERATOR;
+ }
+
+ if (!$parent instanceof RestrictionGroup && $parent !== null) {
+ throw new PrivateException("A restriction's parent can only be a group", 1804292129);
+ }
+
+ if (!$field instanceof QueryField) {
+ throw new PrivateException("Invalid field");
+ }
+
+ $this->parent = $parent;
+ $this->field = $field;
+ $this->value = $value;
+ $this->operator = trim($operator);
+ }
+
+ public function getTable(){
+ return $this->field->getField()->getTable();
+ }
+
+ public function setTable() {
+ throw new PrivateException('Deprecated');
+ }
+
+ public function getField() {
+ return $this->field;
+ }
+
+ /**
+ * Returns the query this restriction belongs to. This allows a query to
+ * define an alias for the table in order to avoid collissions.
+ *
+ * @return Query
+ */
+ public function getQuery() {
+ return $this->parent->getQuery();
+ }
+
+ public function getParent() {
+ return $this->parent;
+ }
+
+ public function setParent($parent) {
+ $this->parent = $parent;
+ }
+
+ /**
+ *
+ * @param Query $query
+ * @deprecated since version 0.1-dev 1604162323
+ */
+ public function setQuery($query) {
+ $this->parent = $query;
+ $this->field->setQuery($query);
+ }
+
+ public function getOperator() {
+ if (is_array($this->value) && $this->operator != 'IN' && $this->operator != 'NOT IN') return 'IN';
+ return $this->operator;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+
+ public function getPhysicalSubqueries() {
+ return Array();
+ }
+
+
+ public function getSubqueries() {
+ return Array();
+ }
+
+ public function getConnectingRestrictions() {
+ return Array();
+ }
+
+ public function replaceQueryTable($old, $new) {
+
+ if ($this->field->getQueryTable() === $old) {
+ $this->field->setQueryTable($new);
+ }
+
+ if ($this->value instanceof QueryField && $this->value->getQueryTable() === $old) {
+ $this->value->setQueryTable($new);
+ }
+ }
+
+ public function negate() {
+ switch ($this->operator) {
+ case '=':
+ return $this->operator = '<>';
+ case '<>':
+ return $this->operator = '=';
+ case '>':
+ return $this->operator = '<';
+ case '<':
+ return $this->operator = '>';
+ case 'IS':
+ return $this->operator = 'IS NOT';
+ case 'IS NOT':
+ return $this->operator = 'IS';
+ case 'LIKE':
+ return $this->operator = 'NOT LIKE';
+ case 'NOT LIKE':
+ return $this->operator = 'LIKE';
+ }
+ }
+
+ /**
+ * Restrictions must be able to be casted to string. This is not only often
+ * necessary for many drivers to generate queries but also for debugging.
+ *
+ * @return string
+ */
+ abstract public function __toString();
+}
diff --git a/src/RestrictionGroup.php b/src/RestrictionGroup.php
new file mode 100644
index 0000000..0f75dde
--- /dev/null
+++ b/src/RestrictionGroup.php
@@ -0,0 +1,295 @@
+<?php namespace spitfire\storage\database;
+
+use InvalidArgumentException;
+use spitfire\core\Collection;
+use spitfire\exceptions\PrivateException;
+
+/**
+ * A restriction group contains a set of restrictions (or restriction groups)
+ * that can be used by the database to generate more complex queries.
+ *
+ * This groups can be different of two different types, they can be 'OR' or 'AND',
+ * changing the behavior of the group by making it more or less restrictive. This
+ * OR and AND types are known from most DBMS.
+ */
+abstract class RestrictionGroup extends Collection
+{
+ const TYPE_OR = 'OR';
+ const TYPE_AND = 'AND';
+
+ private $parent;
+ private $type = self::TYPE_OR;
+ private $negated = false;
+
+ public function __construct(RestrictionGroup$parent = null, $restrictions = Array() ) {
+ $this->parent = $parent;
+ parent::__construct($restrictions);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20170720
+ * @remove 20180711
+ * @param type $restriction
+ */
+ public function putRestriction($restriction) {
+ parent::push($restriction);
+ }
+
+ /**
+ * Adds a restriction to the current query. Restraining the data a field
+ * in it can contain.
+ *
+ * @todo This method does not accept logical fields as parameters
+ * @see http://www.spitfirephp.com/wiki/index.php/Method:spitfire/storage/database/Query::addRestriction
+ *
+ * @deprecated since version 0.1-dev 20170923
+ * @remove 20180711
+ * @param string $fieldname
+ * @param mixed $value
+ * @param string $operator
+ * @return RestrictionGroup
+ * @throws PrivateException
+ */
+ public function addRestriction($fieldname, $value, $operator = '=') {
+ return $this->where($fieldname, $operator, $value);
+ }
+
+ /**
+ * Adds a restriction to the current query. Restraining the data a field
+ * in it can contain.
+ *
+ * @todo This method does not accept logical fields as parameters
+ * @see http://www.spitfirephp.com/wiki/index.php/Method:spitfire/storage/database/Query::addRestriction
+ *
+ * @param string $fieldname
+ * @param mixed $value
+ * @param string $_
+ * @return RestrictionGroup
+ * @throws PrivateException
+ */
+ public function where($fieldname, $value, $_ = null) {
+ $params = func_num_args();
+ $rm = $this->getQuery()->getTable()->getDb()->getRestrictionMaker();
+
+ /*
+ * Depending on how the parameters are provided, where will appropriately
+ * shuffle them to make them look correctly.
+ */
+ if ($params === 3) { list($operator, $value) = [$value, $_]; }
+ else { $operator = '='; }
+
+ $this->push($rm->make($this, $fieldname, $operator, $value));
+ return $this;
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20170720
+ * @param type $restrictions
+ */
+ public function getRestrictions() {
+ return parent::toArray();
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20170720
+ * @param type $index
+ */
+ public function getRestriction($index) {
+ return parent::offsetGet($index);
+ }
+
+ /**
+ *
+ */
+ public function getCompositeRestrictions() {
+
+ return $this->each(function ($r) {
+ if ($r instanceof CompositeRestriction) { return $r; }
+ if ($r instanceof RestrictionGroup) { return $r->getCompositeRestrictions(); }
+ return null;
+ })
+ ->flatten()
+ ->filter(function ($e) {
+ return $e !== null && ($e instanceof CompositeRestriction || !$e->isEmpty());
+ });
+ }
+
+ /**
+ * @param string $type
+ * @return RestrictionGroup
+ */
+ public function group($type = self::TYPE_OR) {
+ #Create the group and set the type we need
+ $group = $this->getQuery()->getTable()->getDb()->getObjectFactory()->restrictionGroupInstance($this);
+ $group->setType($type);
+
+ #Add it to our restriction list
+ return $this->push($group);
+ }
+
+ public function endGroup() {
+ return $this->parent;
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20180420
+ * @param \spitfire\storage\database\Query $query
+ * @return $this
+ */
+ public function setQuery(Query$query) {
+ $this->parent = $query;
+ return $this;
+ }
+
+ public function setParent(RestrictionGroup$query) {
+ $this->parent = $query;
+
+ return $this;
+ }
+
+ public function getParent() {
+ return $this->parent;
+ }
+
+ /**
+ * As opposed to the getParent method, the getQuery method will ensure that
+ * the return is a query.
+ *
+ * This allows the application to quickly get information about the query even
+ * if the restrictions are inside of several layers of restriction groups.
+ *
+ * @return Query
+ */
+ public function getQuery() {
+ return $this->parent->getQuery();
+ }
+
+ public function setType($type) {
+ if ($type === self::TYPE_AND || $type === self::TYPE_OR) {
+ $this->type = $type;
+ return $this;
+ }
+ else {
+ throw new InvalidArgumentException("Restriction groups can only be of type AND or OR");
+ }
+ }
+
+ public function getType() {
+ return $this->type;
+ }
+
+ public function getSubqueries() {
+
+ /*
+ * First, we extract the physical queries from the underlying queries.
+ * These queries should be executed first, to make it easy for the system
+ * to retrieve the data the query depends on.
+ */
+ $_ret = Array();
+
+ foreach ($this as $r) {
+ $_ret = array_merge($_ret, $r->getSubqueries());
+ }
+
+ return $_ret;
+ }
+
+ public function replaceQueryTable($old, $new) {
+
+
+ foreach ($this->getRestrictions() as $r) {
+ $r->replaceQueryTable($old, $new);
+ }
+ }
+
+ public function negate() {
+ $this->negated = !$this->negated;
+ return $this;
+ }
+
+ public function normalize() {
+ if ($this->negated) {
+ $this->flip();
+ }
+
+ $this
+ /*
+ * We normalize the children first. This ensures that the normalization
+ * the parent performs is still correct.
+ */
+ ->filter(function ($e) { return $e instanceof RestrictionGroup; })
+ ->each(function (RestrictionGroup$e) { return $e->normalize(); })
+
+ /*
+ * We remove the groups that satisfy any of the following:
+ * * They're empty
+ * * They only contain one restriction
+ * * They have the same type as the current one. Based on (A AND B) AND C == A AND B AND C
+ */
+ ->filter(function (RestrictionGroup$e) { return $e->getType() === $this->getType() || $e->count() < 2; })
+ ->each(function ($e) {
+ $this->add($e->each(function ($e) { $e->setParent($this); return $e; })->toArray());
+ $this->remove($e);
+ });
+
+ return $this;
+ }
+
+ /**
+ * When a restriction group is flipped, the system will change the type from
+ * AND to OR and viceversa. When doing so, all the restrictions are negated.
+ *
+ * This means that <code>$a == $a->flip()</code> even though they have inverted
+ * types. This is specially interesting for query optimization and negation.
+ *
+ * @return RestrictionGroup
+ */
+ public function flip() {
+ $this->negated = !$this->negated;
+ $this->type = $this->type === self::TYPE_AND? self::TYPE_OR : self::TYPE_AND;
+
+ foreach ($this as $restriction) {
+ if ($restriction instanceof Restriction ||
+ $restriction instanceof CompositeRestriction ||
+ $restriction instanceof RestrictionGroup) { $restriction->negate(); }
+ }
+
+ return $this;
+ }
+
+ public function isMixed() {
+ $found = false;
+
+ foreach ($this as $r) {
+ if ($r instanceof RestrictionGroup && ($r->getType() !== $this->getType() || $r->isMixed())) {
+ $found = true;
+ }
+ }
+
+ return $found;
+ }
+
+ /**
+ * When cloning a restriction group we need to ensure that the new restrictions
+ * are assigned to the parent, and not some other object.
+ *
+ * TODO: This would be potentially much simpler if the collection provided a
+ * walk method that would allow to modify the elements from within.
+ */
+ public function __clone() {
+ $restrictions = $this->toArray();
+
+ foreach ($restrictions as &$r) {
+ $r = clone $r;
+ $r->setParent($this);
+ }
+
+ $this->reset()->add($restrictions);
+ }
+
+ abstract public function __toString();
+}
diff --git a/src/ResultSetInterface.php b/src/ResultSetInterface.php
new file mode 100644
index 0000000..e6e50f8
--- /dev/null
+++ b/src/ResultSetInterface.php
@@ -0,0 +1,66 @@
+<?php namespace spitfire\storage\database;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+use spitfire\Model;
+
+/**
+ * The result set interface defines how the end user application and queries can
+ * interact with result sets. The drivers can use this to provide a mechanism to
+ * access the result cursor of a database query.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+
+interface ResultSetInterface
+{
+ /**
+ * Fetches data from a driver's resultset. This returns a record and advances
+ * the cursor in the database.
+ *
+ * @return Model|null A record of a database, or null if the result is exhausted
+ */
+ public function fetch();
+
+ /**
+ * Returns a raw result, this will usually provide access to the raw data that
+ * the driver is reading from the Database to build models from.
+ *
+ * This method is usually extremely valuable when retrieving aggregates, since
+ * these methods do not fit into models.
+ *
+ * @return mixed[]
+ */
+ public function fetchArray();
+
+ /**
+ * Returns an array containing all the values from the database's result. This
+ * allows your application to loop over the records instead of while-ing over
+ * them.
+ *
+ * @return \spitfire\core\Collection All the models the driver read from the database.
+ */
+ public function fetchAll();
+}
diff --git a/src/Schema.php b/src/Schema.php
new file mode 100644
index 0000000..f859b22
--- /dev/null
+++ b/src/Schema.php
@@ -0,0 +1,315 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\fields\IntegerField;
+use spitfire\core\Collection;
+use spitfire\exceptions\PrivateException;
+use spitfire\model\Field;
+use spitfire\model\Index;
+use spitfire\storage\database\Query;
+use spitfire\storage\database\Table;
+
+/**
+ * A Schema is a class used to define how Spitfire stores data into a DBMS. We
+ * usually consider a DBMS as relational database engine, but Spitfire can
+ * be connected to virtually any engine that stores data. Including No-SQL
+ * databases and directly on the file system. You should even be able to use
+ * tapes, although that would be extra slow.
+ *
+ * Every model contains fields and references. Fields are direct data-types, they
+ * allow storing things directly into them, while references are pointers to
+ * other models allowing you to store more complex data into them.
+ *
+ * @property IntegerField $_id This default primary key integer helps the system
+ * locating the records easily.
+ *
+ * @todo Add index support, so models can create indexes that are somewhat more complex
+ * @author César de la Cal <cesar@magic3w.com>
+ */
+class Schema
+{
+
+ /**
+ * Contains a list of the fields that this model uses t ostore data.
+ * Fields are stored in a FILO way, so the earlier you register a field
+ * the further left will it be on a database table (if you look at it
+ * in table mode).
+ *
+ * @var Field[]
+ */
+ private $fields;
+
+ /**
+ * The indexes the table can use to optimize the search performance.
+ *
+ * @var Collection <Index>
+ */
+ private $indexes;
+
+ /**
+ * Contains a reference to the table this model is 'templating'. This
+ * means that the current model is attached to said table and offers to
+ * it information about the data that is stored to the DBMS and the format
+ * it should hold.
+ *
+ * @var Table
+ */
+ private $table;
+
+ /**
+ * The name of the table that represents this schema on DBMS' side. This will
+ * be automatically generated from the class name and will be replacing the
+ * invalid inverted bar (\) with hyphens (-) that are not valid as a class name.
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Creates a new instance of the Model. This allows Spitfire to create
+ * and manage data accordingly to your wishes on a DB engine without
+ * requiring you to integrate with any concrete engine but writing code
+ * that SF will translate.
+ *
+ * @param string $name
+ * @param Table $table
+ */
+ public final function __construct($name, Table$table = null) {
+ #Define the Model's table as the one just received
+ $this->table = $table;
+ $this->name = strtolower($name);
+
+ #Create a field called ID that automatically identifies records
+ $this->_id = new IntegerField(true);
+ $this->_id->setAutoIncrement(true);
+
+ #Create a default index for the primary key
+ $pk = new Index([$this->_id]);
+ $pk->setPrimary(true);
+
+ #Create the index collection
+ $this->indexes = new Collection([$pk]);
+ }
+
+ /**
+ * Imports a set of fields. This allows to back them up in case they're
+ * needed. Please note that the parent setting for them will be rewritten.
+ *
+ * @param Field[] $fields
+ */
+ public function setFields($fields) {
+ #Loop through the fields to import them
+ foreach($fields as $field) {
+ $this->{$field->getName()} = $field; #This triggers the setter
+ }
+ }
+
+ /**
+ * Returns a logical field for this model. "Logical field" refers to fields
+ * that can also contain complex datatypes aka References.
+ *
+ * You can use the Field::getPhysical() method to retrieve the physical fields
+ * the application uses to interact with the DBMS.
+ *
+ * @param string $name
+ * @return Field|null
+ */
+ public function getField($name) {
+ if (isset($this->fields[$name])) { return $this->fields[$name]; }
+ else { return null; }
+ }
+
+ /**
+ * Returns the whole list of fields this model contains. This are logical fields
+ * and therefore can contain data that is too complex to be stored directly
+ * by a DB Engine, the table object is in charge of providing a list of
+ * DB Friendly fields.
+ *
+ * @return Field[]
+ */
+ public function getFields() {
+ return $this->fields;
+ }
+
+ /**
+ * Returns the 'name' of the model. The name of a model is obtained by
+ * removing the Model part of tit's class name. It's best practice to
+ * avoid the usage of this function for anything rather than logging.
+ *
+ * This function has a special use case, it also defines the name of the
+ * future table. By changing this you change the table this model uses
+ * on DBMS, this is particularly useful when creating multiple models
+ * that refer to a single dataset like 'People' and 'Adults'.
+ *
+ * @staticvar string $name
+ * @return string
+ */
+ public final function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Returns the tablename spitfire considers best for this Model. This
+ * value is calculated by using the Model's name and replacing any
+ * <b>\</b> with hyphens to make the name database friendly.
+ *
+ * Hyphens are the only chars that DBMS tend to accept that class names
+ * do not. So this way we avoid any colissions in names that could be
+ * coincidentally similar.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return trim(str_replace('\\', '-', $this->getName()), '-_ ');
+ }
+
+ /**
+ * Returns the table the Schema represents. The schema is the logical representation
+ * of a Table. While the Schema will manage logical fields that the programmer
+ * can directly write data to, the Table will take that data and translate it
+ * so the database engine can use it.
+ *
+ * @return Table
+ */
+ public function getTable() {
+ return $this->table;
+ }
+
+ /**
+ * Sets the table this schema manages. This connection is used to determine
+ * what DBMS table it should address and to make correct data conversion.
+ *
+ * @param Table $table
+ */
+ public function setTable($table) {
+ $this->table = $table;
+ }
+
+ /**
+ * Extending this function on models allows you to add restrictions to a
+ * query when this is made for this model. This way you can generate a
+ * behavior similar to a view where the records are always limited by
+ * the restriction set in this function.
+ *
+ * This is especially useful when fake deleting records from the database.
+ * You use a flag to indicate a record is deleted and use this function
+ * to hide any records that have that flag.
+ *
+ * @todo Move to the model. It makes no sense having it here
+ * @param Query $query The query that is being prepared to be executed.
+ * @return type
+ */
+ public function getBaseRestrictions(Query$query) {
+ //Do nothing, this is meant for overriding
+ }
+
+ public function index() {
+ $fields = func_get_args();
+ $index = new Index($fields);
+
+ $this->indexes->push($index);
+ return $index;
+ }
+
+ /**
+ * Returns the collection of indexes that are contained in this model.
+ *
+ * @return Collection <Index>
+ */
+ public function getIndexes() {
+ return $this->indexes;
+ }
+
+ /**
+ * Returns a list of fields which compound the primary key of this model.
+ * The primary key is a set of records that identify a unique record.
+ *
+ * @return Index
+ */
+ public function getPrimary() {
+ #Fetch the field list
+ $indexes = $this->indexes;
+
+ #Loop over the indexes and get the primary one
+ foreach ($indexes as $index) {
+ if ($index->isPrimary()) { return $index; }
+ }
+
+ #If there was no index, then return a null value
+ return null;
+ }
+
+ /**
+ * The getters and setters for this class allow us to create fields with
+ * a simplified syntax and access them just like they were properties
+ * of the object. Please note that some experts recommend avoiding magic
+ * methods for performance reasons. In this case you can use the field()
+ * method.
+ *
+ * @param string $name
+ * @param Field $value
+ */
+ public function __set($name, $value) {
+ /*
+ * First we need to check if the field already exists. In the event of us
+ * overwriting the field we need to remove it from the already existing
+ * indexes
+ */
+ if (isset($this->fields[$name])) {
+ unset($this->$name);
+ }
+
+ /*
+ * Check if the schema received a field. Because if that's not the case we
+ * can't deal with it properly.
+ */
+ if (!$value instanceof Field) {
+ throw new PrivateException('Schema received something else than a field', 1710181717);
+ }
+
+ $value->setName($name);
+ $value->setSchema($this);
+ $this->fields[$name] = $value;
+
+ }
+
+ /**
+ * The getters and setters for this class allow us to create fields with
+ * a simplified syntax and access them just like they were properties
+ * of the object. Please note that some experts recommend avoiding magic
+ * methods for performance reasons. In this case you can use the field()
+ * method.
+ *
+ * @param string $name
+ * @throws PrivateException
+ * @return Field
+ */
+ public function __get($name) {
+ if (isset($this->fields[$name])) { return $this->fields[$name]; }
+ else { throw new PrivateException('Schema: No field ' . $name . ' found'); }
+ }
+
+ /**
+ * Removes a field from the Schema. This is a somewhat rare method, since you
+ * should avoid it's usage in production environments and you should REALLY
+ * know what you're doing before using it.
+ *
+ * @param string $name
+ * @throws PrivateException
+ */
+ public function __unset($name) {
+ #Check if the field actually exists.
+ if (!isset($this->fields[$name])) {
+ throw new PrivateException('Schema: Could not delete. No field ' . $name . ' found');
+ }
+
+ #Get the field
+ $f = $this->fields[$name];
+ unset($this->fields[$name]);
+
+ #Find an index that may contain the field and remove it too
+ $this->indexes = $this->indexes->filter(function ($e) use ($f) {
+ return !$e->contains($f);
+ });
+ }
+
+}
diff --git a/src/Settings.php b/src/Settings.php
new file mode 100644
index 0000000..8f18d8b
--- /dev/null
+++ b/src/Settings.php
@@ -0,0 +1,193 @@
+<?php namespace spitfire\storage\database;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * This class allows to retrieve database settings in a organized manner. This
+ * should make the transfer to environment based database settings much easier.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+class Settings
+{
+
+ private $driver;
+ private $server;
+ private $port;
+ private $user;
+ private $password;
+ private $schema;
+ private $prefix;
+ private $encoding;
+
+ private static $defaults = [
+ 'driver' => 'mysqlpdo',
+ 'server' => 'localhost',
+ 'port' => null,
+ 'user' => 'root',
+ 'password' => '',
+ 'schema' => 'database',
+ 'prefix' => '',
+ 'encoding' => 'utf8'
+ ];
+
+ public function __construct($driver, $server, $port, $user, $password, $schema, $prefix, $encoding) {
+ $this->driver = $driver;
+ $this->server = $server;
+ $this->port = $port;
+ $this->user = $user;
+ $this->password = $password;
+ $this->schema = $schema;
+ $this->prefix = $prefix;
+ $this->encoding = $encoding;
+ }
+
+ public function getDriver() {
+ return $this->driver;
+ }
+
+ public function getServer() {
+ return $this->server;
+ }
+
+ public function getUser() {
+ return $this->user;
+ }
+
+ public function getPassword() {
+ return $this->password;
+ }
+
+ public function getSchema() {
+ return $this->schema;
+ }
+
+ public function getPrefix() {
+ return $this->prefix;
+ }
+
+ public function getEncoding() {
+ return $this->encoding;
+ }
+
+ public function getPort() {
+ return $this->port;
+ }
+
+ public function setDriver($driver) {
+ $this->driver = $driver;
+ return $this;
+ }
+
+ public function setServer($server) {
+ $this->server = $server;
+ return $this;
+ }
+
+ public function setPort($port) {
+ $this->port = $port;
+ return $this;
+ }
+
+ public function setUser($user) {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function setPassword($password) {
+ $this->password = $password;
+ return $this;
+ }
+
+ public function setSchema($schema) {
+ $this->schema = $schema;
+ return $this;
+ }
+
+ public function setPrefix($prefix) {
+ $this->prefix = $prefix;
+ return $this;
+ }
+
+ public function setEncoding($encoding) {
+ $this->encoding = $encoding;
+ return $this;
+ }
+
+ /**
+ * Reads the settings from a URL. Since October 2017 we're focusing on providing
+ * URLs to store database credentials, which allow in turn to store the DB
+ * settings outside of the application and on the server itself.
+ *
+ * @todo Move to external URL parser
+ * @param Settings|string $url
+ * @return Settings
+ */
+ public static function fromURL($url) {
+
+ /*
+ * If the parameter provided is already a settings object, it will be
+ * returned as is.
+ */
+ if ($url instanceof Settings) { return $url; }
+
+ /*
+ * Extract the basic data
+ */
+ $driver = parse_url($url, PHP_URL_SCHEME)?: self::$defaults['driver'];
+ $server = parse_url($url, PHP_URL_HOST) ?: self::$defaults['server'];
+ $port = parse_url($url, PHP_URL_PORT) ?: self::$defaults['port'];
+ $user = parse_url($url, PHP_URL_USER) ?: self::$defaults['user'];
+ $password = parse_url($url, PHP_URL_PASS) ?: self::$defaults['password'];
+ $schema = parse_url($url, PHP_URL_PATH) ?: self::$defaults['port'];
+
+ /**
+ * Extract the data that was provided by the URL. Then we should be able
+ * to easily retrieve the data it provides.
+ */
+ parse_str(parse_url($url, PHP_URL_QUERY), $query);
+
+ $prefix = $query['prefix'] ?? self::$defaults['prefix'];
+ $encoding = $query['encoding']?? self::$defaults['encoding'];
+
+ return new Settings($driver, $server, $port, $user, $password, substr($schema, 1), $prefix, $encoding);
+ }
+
+ public static function fromArray($arr) {
+ $ops = $arr + self::$defaults;
+
+ return new Settings(
+ $ops['driver'],
+ $ops['server'],
+ $ops['port'],
+ $ops['user'],
+ $ops['password'],
+ $ops['schema'],
+ $ops['prefix'],
+ $ops['encoding']
+ );
+ }
+
+}
diff --git a/src/Table.php b/src/Table.php
new file mode 100644
index 0000000..f68fd1d
--- /dev/null
+++ b/src/Table.php
@@ -0,0 +1,273 @@
+<?php namespace spitfire\storage\database;
+
+use CoffeeBean;
+use Model;
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\Schema;
+
+/**
+ * This class simulates a table belonging to a database. This way we can query
+ * and handle tables with 'compiler-friendly' code that will inform about errors.
+ *
+ * @author César de la Cal <cesar@magic3w.com>
+ */
+class Table
+{
+
+ /**
+ * A reference to the database driver loaded. This allows the system to
+ * use several databases without the models colliding.
+ *
+ * @var DB
+ */
+ protected $db;
+
+ /**
+ * The model this table uses as template to create itself on the DBMS. This is
+ * one of the key components to Spitfire's ORM as it allows the DB engine to
+ * create the tables automatically and to discover the data relations.
+ *
+ * @var Schema
+ */
+ protected $schema;
+
+ /**
+ * Provides access to the table's layout (physical schema)
+ *
+ * @var LayoutInterface
+ */
+ private $layout = false;
+
+ /**
+ * Provides access to the table's record operations. Basically, a relational
+ * table is composed of schema + relation (data).
+ *
+ * @var Relation
+ */
+ private $relation;
+
+ /**
+ * Contains the bean this table uses to generate forms for itself. The bean
+ * contains additional data to make the data request more user friendly.
+ *
+ * @var CoffeeBean
+ */
+ protected $bean;
+
+ /**
+ * Caches a list of fields that compound this table's primary key. The property
+ * is empty when the table is constructed and collects the primary key's fields
+ * once they are requested for the first time.
+ *
+ * @var \spitfire\storage\database\Index|null
+ */
+ protected $primaryK;
+
+ /**
+ * Just like the primary key field, this property caches the field that contains
+ * the autonumeric field. This will usually be the ID that the DB refers to
+ * when working with the table.
+ *
+ * @var Field
+ */
+ protected $autoIncrement;
+
+ /**
+ * Creates a new Database Table instance. The tablename will be used to find
+ * the right model for the table and will be stored prefixed to this object.
+ *
+ * @param DB $db
+ * @param string|Schema $schema
+ *
+ * @throws PrivateException
+ */
+ public function __construct(DB$db, $schema) {
+ $this->db = $db;
+ $factory = $this->db->getObjectFactory();
+
+ if (!$schema instanceof Schema) {
+ throw new PrivateException('Table requires a Schema to be passed');
+ }
+
+ #Attach the schema to this table
+ $this->schema = $schema;
+ $this->schema->setTable($this);
+
+ #Create a database table layout (physical schema)
+ $this->layout = $factory->makeLayout($this);
+
+ #Create the relation
+ $this->relation = $factory->makeRelation($this);
+ }
+
+ /**
+ * Fetch the fields of the table the database works with. If the programmer
+ * has defined a custom set of fields to work with, this function will
+ * return the overridden fields.
+ *
+ * @return Field[] The fields this table handles.
+ */
+ public function getFields() {
+ trigger_error('Deprecated function Table::getFields() called', E_USER_DEPRECATED);
+ return $this->layout->getFields();
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20171128
+ * @param type $name
+ * @return type
+ */
+ public function getField($name) {
+ trigger_error('Deprecated function Table::getField() called', E_USER_DEPRECATED);
+ return $this->layout->getField($name);
+ }
+
+ /**
+ * Returns the database the table belongs to.
+ * @return DB
+ */
+ public function getDb() {
+ return $this->db;
+ }
+
+ /**
+ * Get's the table's primary key. This will always return an array
+ * containing the fields the Primary Key contains.
+ *
+ * @return IndexInterface
+ */
+ public function getPrimaryKey() {
+ /*
+ * If the primary was already determined, we use the cached version.
+ */
+ if ($this->primaryK) { return $this->primaryK; }
+
+ $indexes = $this->layout->getIndexes();
+ return $this->primaryK = $indexes->filter(function (IndexInterface$i) { return $i->isPrimary(); })->rewind();
+ }
+
+ public function getAutoIncrement() {
+ if ($this->autoIncrement) { return $this->autoIncrement; }
+
+ //Implicit else
+ $fields = $this->layout->getFields();
+
+ foreach($fields as $field) {
+ if ($field->getLogicalField()->isAutoIncrement()) { return $this->autoIncrement = $field; }
+ }
+
+ return null;
+ }
+
+ /**
+ * Looks for a record based on it's primary data. This can be one of the
+ * following:
+ * <ul>
+ * <li>A single basic data field like a string or a int</li>
+ * <li>A string separated by : to separate those fields (SF POST standard)</li>
+ * <li>An array with the data</li>
+ * </ul>
+ * This function is intended to be used to provide controllers with prebuilt
+ * models so they don't need to fetch it again.
+ *
+ * @todo Move to relation
+ *
+ * @param mixed $id
+ *
+ * @return Model
+ */
+ public function getById($id) {
+ #If the data is a string separate by colons
+ if (!is_array($id)) { $id = explode(':', $id); }
+
+ #Create a query
+ $table = $this;
+ $primary = $table->getPrimaryKey()->getFields();
+ $query = $table->getDb()->getObjectFactory()->queryInstance($this);
+
+ #Add the restrictions
+ while(!$primary->isEmpty()) {
+ $query->where($primary->shift(), array_shift($id));
+ }
+
+ #Return the result
+ $_return = $query->fetch();
+
+ return $_return;
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20160902
+ * @return Schema
+ */
+ public function getModel() {
+ return $this->schema;
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20170801
+ * @return Relation
+ */
+ public function getCollection() {
+ return $this->relation;
+ }
+
+ /**
+ * Gives access to the relation, the table's component that manages the data
+ * that the table contains.
+ *
+ * @return Relation
+ */
+ public function getRelation() {
+ return $this->relation;
+ }
+
+ /**
+ *
+ * @return LayoutInterface
+ */
+ public function getLayout(): LayoutInterface {
+ return $this->layout;
+ }
+
+ /**
+ *
+ * @return Schema
+ */
+ public function getSchema() {
+ return $this->schema;
+ }
+
+ /**
+ * Returns the bean this model uses to generate Forms to feed itself with data
+ * the returned value normally is a class that inherits from CoffeeBean.
+ *
+ * @deprecated since version 0.1-dev 20161220
+ * @return CoffeeBean
+ */
+ public function getBean($name = null) {
+
+ if (!$name) { $beanName = $this->schema->getName() . 'Bean'; }
+ else { $beanName = $name . 'Bean'; }
+
+ $bean = new $beanName($this);
+
+ return $bean;
+ }
+
+ public function get($field, $value, $operator = '=') {
+ return $this->relation->get($field, $value, $operator);
+ }
+
+ public function getAll() {
+ return $this->relation->getAll();
+ }
+
+ public function newRecord($data = Array()) {
+ return $this->relation->newRecord($data);
+ }
+
+}
diff --git a/src/TablePool.php b/src/TablePool.php
new file mode 100644
index 0000000..ec3ac11
--- /dev/null
+++ b/src/TablePool.php
@@ -0,0 +1,134 @@
+<?php namespace spitfire\storage\database;
+
+use InvalidArgumentException;
+use spitfire\cache\MemoryCache;
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\tablelocator\CacheLocator;
+use spitfire\storage\database\tablelocator\NameLocator;
+use spitfire\storage\database\tablelocator\OTFTableLocator;
+use spitfire\storage\database\tablelocator\TableLocatorInterface;
+use spitfire\storage\database\tablelocator\TypoCacheLocator;
+use spitfire\storage\database\tablelocator\TypoLocator;
+use spitfire\utils\Strings;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * Contains a table list for a database. Please note that this system is neither
+ * caps sensitive nor is it plural sensitive. When looking for the table
+ * "deliveries" it will automatically check for "delivery" too.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+class TablePool
+{
+
+ /**
+ * The database this contains tables for. This is important since the database
+ * offloads table "makes" to the pool.
+ *
+ * @var TableLocatorInterface[]
+ */
+ private $tableLocators;
+
+ private $cache;
+
+ /**
+ * Creates a new Table pool object. This object is designed to cache tables
+ * across several queries, allowing for them to refer to the same schemas and
+ * data-caches that the tables provide.
+ *
+ * @param DB $db
+ */
+ public function __construct(DB$db) {
+ $this->cache = new MemoryCache();
+
+ $this->tableLocators = [
+ new CacheLocator($this->cache),
+ new NameLocator($db),
+ new TypoCacheLocator($this->cache, function ($e) { return Strings::singular($e); }),
+ new TypoCacheLocator($this->cache, function ($e) { return Strings::plural($e); }),
+ new TypoLocator($db, function ($e) { return Strings::singular($e); }),
+ new TypoLocator($db, function ($e) { return Strings::plural($e); }),
+ new OTFTableLocator($db)
+ ];
+ }
+
+ /**
+ * Pushes a table into the pool. This method will check that it's receiving a
+ * proper table object.
+ *
+ * @param string $key
+ * @param Table $value
+ * @return Table
+ * @throws InvalidArgumentException
+ */
+ public function set($key, $value) {
+ if (!$value instanceof Table) {
+ throw new InvalidArgumentException('Table is required');
+ }
+
+ return $this->cache->set(strtolower($key), $value);
+ }
+
+ /**
+ * Returns the Table that the user is requesting from the pool. The pool will
+ * automatically check if the table was misspelled.
+ *
+ * @param string $key
+ * @return Table
+ * @throws PrivateException
+ */
+ public function get($key) {
+ $table = false;
+ $locators = $this->tableLocators;
+
+ while (!$table && $locators) {
+ $locator = array_shift($locators);
+ $table = $locator->locate($key);
+ }
+
+ if ($table) {
+ return $this->set($key, $table);
+ }
+
+ throw new PrivateException(sprintf('Table %s was not found', $key));
+ }
+
+ public function contains($key) {
+
+ try {
+ $this->get($key);
+ return true;
+ } catch (PrivateException $ex) {
+ return false;
+ }
+ }
+
+ public function getCache() {
+ return $this->cache;
+ }
+
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/CompositeRestriction.php b/src/drivers/mysqlpdo/CompositeRestriction.php
new file mode 100644
index 0000000..a209632
--- /dev/null
+++ b/src/drivers/mysqlpdo/CompositeRestriction.php
@@ -0,0 +1,57 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\CompositeRestriction as ParentClass;
+use spitfire\storage\database\RestrictionGroup;
+
+class CompositeRestriction extends ParentClass
+{
+
+ /**
+ * When a query is serialized, the composite restrictions generate a list of
+ * simple ones that can be passed onto the database for querying.
+ *
+ * In the case of MySQLPDO, the driver assumes that the query has been properly
+ * denormalized to be serialized.
+ *
+ * @return RestrictionGroup
+ */
+ public function makeSimpleRestrictions() {
+
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
+
+ /*
+ * Extract the primary fields for the remote table so we can indicate to the
+ * database whether they should be null or not.
+ *
+ * Please note that we will always use "IS NOT NULL" so the connectors stay
+ * consistent with the rest of the restrictions
+ */
+ $fields = $this->getValue()->getQueryTable()->getTable()->getPrimaryKey()->getFields();
+ $group = $of->restrictionGroupInstance($this->getParent());
+
+ /*
+ * Loop over the fields and put them in an array so it can be concatenated
+ * before being returned.
+ */
+ foreach($fields as $field) {
+ $qt = $this->getValue()->getRedirection()? $this->getValue()->getRedirection()->getQueryTable() : $this->getValue()->getQueryTable();
+ $group->push($of->restrictionInstance($group, $of->queryFieldInstance($qt, $field), null, $this->getOperator() === '='? 'IS NOT' : 'IS'));
+ }
+
+ return $group;
+ }
+
+ public function __toString() {
+ $field = $this->getField();
+ $value = $this->getValue();
+
+ if ($field === null || $value === null) {
+ throw new PrivateException('Deprecated: Composite restrictions do not receive null parameters', 2801191504);
+ }
+
+ return strval($this->makeSimpleRestrictions());
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/Driver.php b/src/drivers/mysqlpdo/Driver.php
new file mode 100644
index 0000000..c6f5e58
--- /dev/null
+++ b/src/drivers/mysqlpdo/Driver.php
@@ -0,0 +1,197 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use PDO;
+use PDOException;
+use PDOStatement;
+use spitfire\exceptions\FileNotFoundException;
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\DB;
+use function spitfire;
+
+/**
+ * MySQL driver via PDO. This driver does <b>not</b> make use of prepared
+ * statements, prepared statements become too difficult to handle for the driver
+ * when using several JOINs or INs. For this reason the driver has moved from
+ * them back to standard querying.
+ */
+class Driver extends DB
+{
+
+ private $connection = false;
+
+ /**@var mixed List of errors the repair() method can fix. This include:
+ * <ul>
+ * <li>1051 - Unknown table.</li>
+ * <li>1054 - Unknown column</li>
+ * <li>1146 - No such table</li>
+ * </ul>
+ */
+ private $reparableErrors = Array(1051, 1054, 1146);
+
+
+ /**
+ * Establishes the connection with the database server. This function
+ * requires no parameters as they're stored by the class already.
+ *
+ * @return boolean
+ * @throws PrivateException If the database was unable to establish a
+ * connection because the Server rejected the connection.
+ */
+ protected function connect() {
+ $settings = $this->getSettings();
+
+ $encoding = ['utf8' => 'utf8mb4'][$this->getEncoder()->getInnerEncoding()];
+
+ $dsn = 'mysql:' . http_build_query(array_filter(['dbname' => $settings->getSchema(), 'host' => $settings->getServer(), 'charset' => $encoding]), '', ';');
+ $user = $settings->getUser();
+ $pass = $settings->getPassword();
+
+ try {
+ $this->connection = new PDO($dsn, $user, $pass);
+ $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $this->connection->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_NATURAL);
+
+ return true;
+ } catch (PDOException $e) {
+
+ if ($e->errorInfo == null) { //Apparently a error in
+ throw new FileNotFoundException('Database does not exist', 1709051253);
+ }
+
+ spitfire()->log($e->getMessage());
+ throw new PrivateException('DB Error. Connection refused by the server: ' . $e->getMessage());
+ }
+
+ }
+
+ /**
+ * Checks if a connection to the DB exists and creates it in case it
+ * does not exist already.
+ *
+ * @return PDO
+ */
+ public function getConnection() {
+ if (!$this->connection) { $this->connect(); }
+ return $this->connection;
+ }
+
+ /**
+ * Sends a query to the database server and returns the handle for the
+ * resultset the server / native driver returned.
+ *
+ * @param string $statement SQL to be executed by the server.
+ * @param boolean $attemptrepair Defines whether the server should try
+ * to repair any model inconsistencies the server
+ * encounters.
+ * @return PDOStatement
+ * @throws PrivateException In case the query fails for another reason
+ * than the ones the system manages to fix.
+ */
+ public function execute($statement, $parameters = Array(), $attemptrepair = true) {
+ #Connect to the database and prepare the statement
+ $con = $this->getConnection();
+
+ try {
+ spitfire()->log("DB: " . $statement);
+ #Execute the query
+ $stt = $con->prepare($statement);
+ $stt->execute();
+
+ return $stt;
+
+ } catch(PDOException $e) {
+ #Log the error that happened.
+ spitfire()->log("Captured: {$e->getCode()} - {$e->getMessage()}");
+ #Recover from exception, make error readable. Re-throw
+ $code = $e->getCode();
+ $err = $e->errorInfo;
+ $msg = $err[2]? $err[2] : 'Unknown error';
+
+ #If the error is not repairable or the system is blocking repairs throw an exception
+ if (!in_array($err[1], $this->reparableErrors) || !$attemptrepair)
+ { throw new PrivateException("Error {$code} [{$msg}] captured. Not repairable", 1511081930, $e); }
+
+ #Try to solve the error by checking integrity and repeat
+ $this->repair();
+ return $this->execute($statement, $parameters, false);
+ }
+ }
+
+ /**
+ * Escapes a string to be used in a SQL statement. PDO offers this
+ * functionality out of the box so there's nothing to do.
+ *
+ * @param string $text
+ * @return string Quoted and escaped string
+ */
+ public function quote($text) {
+ if ($text === null) { return 'null'; }
+ if ($text === 0) { return "'0'"; }
+ if ($text === false) { return "'0'"; }
+
+ $str = $this->getEncoder()->encode($text); //This statement should not be here.
+ //It's not part of the quoting mechanism to encode the data.
+
+ return $this->getConnection()->quote( $str );
+ }
+
+ /**
+ *
+ * @staticvar \storage\database\drivers\mysqlpdo\ObjectFactory $factory
+ * @return ObjectFactory
+ */
+ public function getObjectFactory() {
+ static $factory;
+ return $factory? : $factory = new ObjectFactory();
+ }
+
+ /**
+ * Creates a database on MySQL's side where data can be stored on behalf of
+ * the application.
+ *
+ * @return bool
+ */
+ public function create(): bool {
+
+ try {
+ $this->execute(sprintf('CREATE DATABASE `%s`', $this->getSettings()->getSchema()));
+ $this->execute(sprintf('use `%s`;', $this->getSettings()->getSchema()));
+ return true;
+ }
+ /*
+ * Sometimes the database will issue a FileNotFound exception when attempting
+ * to connect to a DBMS that fails if the database it expected to connect
+ * to is not available.
+ *
+ * In this event we create a new connection that ignores the schema setting,
+ * therefore allowing to connect to the database properly.
+ */
+ catch (FileNotFoundException$e) {
+ #Modify the connection settings, removing the schema.
+ $settings = clone $this->getSettings();
+ $settings->setSchema('');
+
+ #Establish the new connection
+ $db = new Driver($settings);
+ $db->getConnection();
+
+ #Set the schema and run a retry
+ $settings->setSchema($this->getSettings()->getSchema());
+ $db->create();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Destroys the database housing the app's information.
+ *
+ * @return bool
+ */
+ public function destroy(): bool {
+ $this->execute(sprintf('DROP DATABASE `%s`', $this->getSettings()->getSchema()));
+ return true;
+ }
+
+}
diff --git a/src/drivers/mysqlpdo/Field.php b/src/drivers/mysqlpdo/Field.php
new file mode 100644
index 0000000..1c08709
--- /dev/null
+++ b/src/drivers/mysqlpdo/Field.php
@@ -0,0 +1,67 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\model\Field as LogicalField;
+use spitfire\storage\database\Field as ParentClass;
+use spitfire\model\fields\Reference;
+
+class Field extends ParentClass
+{
+
+ public function columnType() {
+ $logical = $this->getLogicalField();
+
+ if ($logical instanceof Reference) {
+ $referenced = $this->getReferencedField();
+ while($referenced->getReferencedField()) { $referenced = $referenced->getReferencedField(); }
+
+ $logical = $referenced->getLogicalField();
+ }
+
+ switch ($logical->getDataType()) {
+ case LogicalField::TYPE_INTEGER:
+ return 'INT(11)';
+ case LogicalField::TYPE_FLOAT:
+ return 'DOUBLE';
+ case LogicalField::TYPE_LONG:
+ return 'BIGINT';
+ case LogicalField::TYPE_STRING:
+ return "VARCHAR({$logical->getLength()})";
+ case LogicalField::TYPE_FILE:
+ return "VARCHAR(255)";
+ case LogicalField::TYPE_TEXT:
+ return "TEXT";
+ case LogicalField::TYPE_DATETIME:
+ return "DATETIME";
+ case LogicalField::TYPE_BOOLEAN:
+ return "TINYINT(4)";
+ }
+ }
+
+ public function columnDefinition() {
+ $definition = $this->columnType();
+
+ if (!$this->getLogicalField()->getNullable()) $definition.= " NOT NULL ";
+ if ($this->getLogicalField()->isAutoIncrement()) $definition.= "AUTO_INCREMENT ";
+
+ return $definition;
+ }
+
+ public function add() {
+ $stt = "ALTER TABLE `{$this->getTable()->getLayout()->getTableName()}`
+ ADD COLUMN (`{$this->getName()}` {$this->columnDefinition()} )";
+ $this->getTable()->getDb()->execute($stt);
+
+ if ($this->getLogicalField()->isPrimary()) {
+ $pk = implode(', ', array_keys($this->getTable()->getPrimaryKey()));
+ $stt = "ALTER TABLE {$this->getTable()->getLayout()->getTableName()}
+ DROP PRIMARY KEY,
+ ADD PRIMARY KEY(" . $pk . ")";
+ $this->getTable()->getDb()->execute($stt);
+ }
+ }
+
+ public function __toString() {
+ return "`{$this->getTable()->getLayout()->getTableName()}`.`{$this->getName()}`";
+ }
+
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/ForeignKey.php b/src/drivers/mysqlpdo/ForeignKey.php
new file mode 100644
index 0000000..bebd692
--- /dev/null
+++ b/src/drivers/mysqlpdo/ForeignKey.php
@@ -0,0 +1,96 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\core\Collection;
+use spitfire\storage\database\Field;
+use spitfire\storage\database\ForeignKeyInterface;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * A foreign key is a special index in MySQL that allows the system to improve
+ * performance on joins and to maintain data consistency in the database in
+ * the event of data being modified.
+ *
+ * Spitfire defaults to cascading changes inside the database, since it's models
+ * imply that when a child is orphaned it should be removed. If you still wish
+ * to orphan a model it's recommended you orphan it before removing the parent.
+ *
+ * This is similar to how you would expect the garbage collector of a object
+ * oriented language to work. If a element is deleted and the children are not
+ * referenced elsewhere we'd expect them to be gone.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+class ForeignKey extends Index implements ForeignKeyInterface
+{
+
+ /**
+ * Retrieves a collection of the fields referenced by this index. The fields
+ * returned are provided in the same order as they are defined.
+ *
+ * @return Collection of database fields
+ */
+ public function getReferenced(): Collection {
+ $fields = $this->getFields();
+ $_ret = new Collection();
+
+ $fields->each(function(Field$e) use ($_ret) {
+ $_ret->push($e->getReferencedField());
+ });
+
+ return $_ret;
+ }
+
+ /**
+ * Returns the name of the index. Since this refers to a remote table we will
+ * just appropriately prefix it.
+ *
+ * @return string
+ */
+ public function getName(): string {
+ return 'foreign_' . parent::getName();
+ }
+
+ /**
+ * When the driver is assembling an SQL statement to assemble the table or
+ * repair it, this method will provide it with a statement to generate the
+ * key.
+ *
+ * @return string
+ */
+ public function definition() {
+ $referenced = $this->getReferenced();
+ $table = $referenced->rewind()->getTable();
+
+ return sprintf(
+ 'FOREIGN KEY `%s` (%s) REFERENCES %s(%s) ON DELETE CASCADE ON UPDATE CASCADE',
+ $this->getName(),
+ $this->getFields()->each(function ($e) { return sprintf('`%s`', $e->getName()); })->join(', '),
+ $table->getLayout(),
+ $referenced->each(function ($e) { return sprintf('`%s`', $e->getName()); })->join(', ')
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/Index.php b/src/drivers/mysqlpdo/Index.php
new file mode 100644
index 0000000..31335b6
--- /dev/null
+++ b/src/drivers/mysqlpdo/Index.php
@@ -0,0 +1,89 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\model\Index as LogicalIndex;
+use spitfire\storage\database\IndexInterface;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * The MySQLPDO friendly implementation of indexes.
+ *
+ */
+class Index implements IndexInterface
+{
+
+ private $logical;
+
+ public function __construct(LogicalIndex$index) {
+ $this->logical = $index;
+ }
+
+ public function getFields() {
+ /*
+ * Prepare an array for the fields.
+ */
+ $arr = new \spitfire\core\Collection();
+
+ /**
+ * Each field has one / many physical fields that need to be brought into
+ * the physical index to be generated.
+ */
+ $this->logical->getFields()->each(function ($p) use ($arr) {
+ $arr->add($p->getPhysical());
+ });
+
+ return $arr;
+ }
+
+ public function getName(): string {
+ return $this->logical->getName();
+ }
+
+ public function isPrimary(): bool {
+ return $this->logical->isPrimary();
+ }
+
+ public function isUnique(): bool {
+ return $this->logical->isUnique();
+ }
+
+ /**
+ *
+ * @return LogicalIndex
+ */
+ public function getLogical() : LogicalIndex {
+ return $this->logical;
+ }
+
+ public function definition() {
+ return sprintf(
+ '%s `%s` (%s)',
+ $this->isPrimary()? 'PRIMARY KEY' : ($this->isUnique()? 'UNIQUE INDEX' : 'INDEX'),
+ $this->getName()? : '',
+ $this->getFields()->each(function ($e) { return sprintf('`%s`', $e->getName()); })->join(', ')
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/Layout.php b/src/drivers/mysqlpdo/Layout.php
new file mode 100644
index 0000000..64ad6b9
--- /dev/null
+++ b/src/drivers/mysqlpdo/Layout.php
@@ -0,0 +1,239 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use Reference;
+use spitfire\cache\MemoryCache;
+use spitfire\core\Collection;
+use spitfire\exceptions\PrivateException;
+use spitfire\model\Index as LogicalIndex;
+use spitfire\storage\database\Field;
+use spitfire\storage\database\IndexInterface;
+use spitfire\storage\database\LayoutInterface;
+use spitfire\storage\database\Table;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * The MySQL PDO driver for the table layouts. This allows Spitfire to create,
+ * destroy and repair tables on a MySQL or MariaDB database.
+ *
+ * @todo This driver contains a lot of legacy code imported from the previous
+ * iteration and would require a lot of fixing and maintaining.
+ */
+class Layout implements LayoutInterface
+{
+ /**
+ * The table that the system uses to connect layout and relation.
+ *
+ * @var Table
+ */
+ private $table;
+
+ /**
+ * The prefixed name of the table. The prefix is defined by the environment
+ * and allows to have several environments on the same database.
+ *
+ * @var string
+ */
+ private $tablename;
+
+ /**
+ * List of the physical fields this table handles. This array is just a
+ * shortcut to avoid looping through model-fields everytime a query is
+ * performed.
+ *
+ * @var Field[]
+ */
+ private $fields;
+
+ /**
+ * An array of indexes that this table defines to manage it's queries and
+ * data.
+ *
+ * @var MemoryCache
+ */
+ private $indexes;
+
+ /**
+ *
+ * @param Table $table
+ */
+ public function __construct(Table$table) {
+ #Assume the table
+ $this->table = $table;
+
+ #Get the physical table name. This will use the prefix to allow multiple instances of the DB
+ $this->tablename = $this->table->getDb()->getSettings()->getPrefix() . $table->getSchema()->getTableName();
+
+ #Create the physical fields
+ $fields = $this->table->getSchema()->getFields();
+ $columns = Array();
+
+ foreach ($fields as $field) {
+ $physical = $field->getPhysical();
+ while ($phys = array_shift($physical)) { $columns[$phys->getName()] = $phys; }
+ }
+
+ $this->fields = $columns;
+ $this->indexes = new MemoryCache();
+ }
+
+ public function create() {
+ $table = $this;
+ $definitions = $table->columnDefinitions();
+ $indexes = $table->getIndexes();
+
+ #Strip empty definitions from the list
+ $clean = array_filter(array_merge(
+ $definitions,
+ $indexes->each(function($e) { return $e->definition(); })->toArray()
+ ));
+
+ $stt = sprintf('CREATE TABLE %s (%s) ENGINE=InnoDB CHARACTER SET=utf8mb4',
+ $table,
+ implode(', ', $clean)
+ );
+
+ return $this->table->getDb()->execute($stt);
+ }
+
+ public function destroy() {
+ $this->table->getDb()->execute('DROP TABLE ' . $this);
+ }
+
+ /**
+ * Fetch the fields of the table the database works with. If the programmer
+ * has defined a custom set of fields to work with, this function will
+ * return the overridden fields.
+ *
+ * @return Field[] The fields this table handles.
+ */
+ public function getFields() {
+ return $this->fields;
+ }
+
+ public function getField($name) : Field {
+ #If the data we get is already a DBField check it belongs to this table
+ if ($name instanceof Field) {
+ if ($name->getTable() === $this->table) { return $name; }
+ else { throw new PrivateException('Field ' . $name . ' does not belong to ' . $this); }
+ }
+
+ if (is_object($name)) {
+ throw new PrivateException('Expected a field name, got an object', 1708101329);
+ }
+
+ #Otherwise search for it in the fields list
+ if (isset($this->fields[(string)$name])) { return $this->fields[(string)$name]; }
+
+ #The field could not be found in the Database
+ throw new PrivateException('Field ' . $name . ' does not exist in ' . $this);
+ }
+
+ /**
+ *
+ * @return Collection <Index>
+ */
+ public function getIndexes() {
+
+ return $this->indexes->get('indexes', function() {
+
+ /*
+ * First we get the defined indexes.
+ */
+ $logical = $this->table->getSchema()->getIndexes();
+ $indexes = $logical->each(function (LogicalIndex$e) {
+ return new Index($e);
+ });
+
+ /*
+ * Then we get those implicitly defined by reference fields. These are
+ * defined by the driver, sicne they're required for it to work.
+ */
+ $fields = array_filter($this->table->getSchema()->getFields(), function($e) { return $e instanceof Reference;});
+
+ foreach ($fields as $field) {
+ $indexes->push(new ForeignKey(new LogicalIndex([$field])));
+ }
+
+ return $indexes;
+ });
+ }
+
+ public function getTableName() : string {
+ return $this->tablename;
+ }
+
+ public function repair() {
+ $stt = "DESCRIBE {$this}";
+ $fields = $this->getFields();
+
+ foreach ($this->table->getSchema()->getFields() as $f) {
+ if ($f instanceof Reference && $f->getTarget() !== $this->table->getSchema()) {
+ $f->getTarget()->getTable()->getLayout()->repair();
+ }
+ }
+ //Fetch the DB Fields and create on error.
+ try {
+ $query = $this->table->getDb()->execute($stt, Array(), false);
+ }
+ catch(\Exception $e) {
+ return $this->create();
+ }
+ //Loop through the exiting fields
+ while (false != ($f = $query->fetch())) {
+ try {
+ $field = $this->getField($f['Field']);
+ unset($fields[$field->getName()]);
+ }
+ catch(\Exception $e) {/*Ignore*/}
+ }
+
+ foreach($fields as $field) $field->add();
+ }
+
+ /**
+ * Creates the column definitions for each column
+ *
+ * @return mixed
+ */
+ protected function columnDefinitions() {
+ $fields = $this->getFields();
+ foreach ($fields as $name => $f) {
+ $fields[$name] = '`'. $name . '` ' . $f->columnDefinition();
+ }
+ return $fields;
+ }
+
+ /**
+ * Returns the name of a table as DB Object reference (with quotes).
+ *
+ * @return string The name of the table escaped and ready for use inside
+ * of a query.
+ */
+ public function __toString() {
+ return "`{$this->tablename}`";
+ }
+
+}
diff --git a/src/drivers/mysqlpdo/ObjectFactory.php b/src/drivers/mysqlpdo/ObjectFactory.php
new file mode 100644
index 0000000..63dddee
--- /dev/null
+++ b/src/drivers/mysqlpdo/ObjectFactory.php
@@ -0,0 +1,163 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use BadMethodCallException;
+use spitfire\exceptions\PrivateException;
+use spitfire\model\Field as LogicalField;
+use spitfire\storage\database\DB;
+use spitfire\storage\database\drivers\mysqlpdo\Field as MysqlField;
+use spitfire\storage\database\drivers\mysqlpdo\Query;
+use spitfire\storage\database\drivers\mysqlpdo\Restriction;
+use spitfire\storage\database\drivers\mysqlpdo\RestrictionGroup;
+use spitfire\storage\database\Field;
+use spitfire\storage\database\LayoutInterface;
+use spitfire\storage\database\ObjectFactoryInterface;
+use spitfire\storage\database\QueryField as AbstractQueryField;
+use spitfire\storage\database\QueryTable as AbstractQueryTable;
+use spitfire\storage\database\Relation as RelationAbstract;
+use spitfire\storage\database\RestrictionGroup as AbstractRestrictionGroup;
+use spitfire\storage\database\Schema;
+use spitfire\storage\database\Table;
+use TextField;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * The object factory class allows a Database to centralize a point where the
+ * database objects can retrieve certain items from. As opposed to having this
+ * algorithms in every class, as some classes would just be overriding one factory
+ * method they needed in a completely standard class.
+ *
+ * This allows Spitfire to define certain behaviors it expects from DB objects
+ * and then have the driver provide this to not disturb Spitfire's logic.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+class ObjectFactory implements ObjectFactoryInterface
+{
+
+ /**
+ * Creates a new on the fly model. This means that the model is created during
+ * runtime, and by reverse engineering the tables that the database already
+ * has.
+ *
+ * Please note, that this model would not perfectly replicate a model you could
+ * build with a proper definition yourself.
+ *
+ * @todo At the time of writing this, the method does not use adequate types.
+ * @param string $modelname
+ * @return Table
+ */
+ public function getOTFSchema(DB$db, $modelname) {
+ #Create a Schema we can feed the data into.
+ $schema = new Schema($modelname);
+
+ #Make the SQL required to read in the data
+ $sql = sprintf('DESCRIBE `%s%s`', $schema->getTableName(), $modelname);
+ /** @var $fields Query */
+ $fields = $db->execute($sql, false);
+
+ while ($row = $fields->fetch()) {
+ $schema->{$row['Field']} = new TextField();
+ }
+
+ return new Table($db, $schema);
+ }
+
+ /**
+ * Creates a new MySQL PDO Field object. This receives the fields 'prototype',
+ * name and reference (in case it references an externa field).
+ *
+ * This represents an actual field in the DBMS as opposed to the ones in the
+ * model. That's why here we talk of "physical" fields
+ *
+ * @todo This should be moved over to a DBMS specific object factory.
+ * @param Field $field
+ * @param string $name
+ * @param Field $references
+ * @return MysqlField
+ */
+ public function getFieldInstance(LogicalField$field, $name, Field$references = null) {
+ return new MysqlField($field, $name, $references);
+ }
+
+ public function restrictionInstance($query, AbstractQueryField$field, $value, $operator = null) {
+ return new Restriction($query, $field, $value, $operator);
+ }
+
+ /**
+ * Makes a new query on a certain table.
+ *
+ * @param Table $table
+ *
+ * @return Query
+ * @throws PrivateException
+ */
+ public function queryInstance($table) {
+ if ($table instanceof RelationAbstract){ $table = $table->getTable(); }
+ if (!$table instanceof Table) { throw new PrivateException('Need a table object'); }
+
+ return new Query($table);
+ }
+
+ public function makeRelation(Table $table) {
+ return new Relation($table);
+ }
+
+ public function __call($name, $args) {
+ throw new BadMethodCallException("Called ObjectFactory::$name. Method does not exist");
+ }
+
+ public function makeLayout(Table $table): LayoutInterface {
+ return new Layout($table);
+ }
+
+ public function restrictionGroupInstance(AbstractRestrictionGroup$parent = null, $type = AbstractRestrictionGroup::TYPE_OR): AbstractRestrictionGroup {
+ $g = new RestrictionGroup($parent);
+ $g->setType($type);
+ return $g;
+ }
+
+
+ public function queryFieldInstance(AbstractQueryTable$queryTable, $field) {
+ if ($field instanceof AbstractQueryField) {return $field; }
+ return new QueryField($queryTable, $field);
+ }
+
+
+ public function queryTableInstance($table) {
+ if ($table instanceof Relation) { $table = $table->getTable(); }
+ if ($table instanceof AbstractQueryTable) { $table = $table->getTable(); }
+
+
+ if (!$table instanceof Table) { throw new PrivateException('Did not receive a table as parameter'); }
+
+ return new QueryTable($table);
+ }
+
+ public function restrictionCompositeInstance(AbstractRestrictionGroup$parent, LogicalField$field = null, $value = null, $operator = null) {
+ return new CompositeRestriction($parent, $field, $value, $operator);
+ }
+
+}
diff --git a/src/drivers/mysqlpdo/Query.php b/src/drivers/mysqlpdo/Query.php
new file mode 100644
index 0000000..df590e0
--- /dev/null
+++ b/src/drivers/mysqlpdo/Query.php
@@ -0,0 +1,184 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\exceptions\PrivateException;
+use spitfire\model\Field;
+use spitfire\storage\database\drivers\mysqlpdo\CompositeRestriction;
+use spitfire\storage\database\drivers\sql\SQLQuery;
+use spitfire\storage\database\QueryField;
+use spitfire\storage\database\QueryTable;
+use spitfire\storage\database\Relation;
+use spitfire\storage\database\Table;
+
+class Query extends SQLQuery
+{
+ public function execute($fields = null, $offset = null, $max = null) {
+
+ $this->setAliased(false);
+
+
+ #Import tables for restrictions from remote queries
+ $plan = $this->makeExecutionPlan();
+ $last = array_shift($plan);
+ $joins = Array();
+ $retModel = empty($fields);
+
+ foreach ($plan as $q) {
+ $joins[] = sprintf('LEFT JOIN %s ON (%s)', $q->getQueryTable()->definition(), implode(' AND ', $q->getRestrictions()));
+ }
+
+ $selectstt = 'SELECT';
+ $fromstt = 'FROM';
+ $tablename = $last->getQueryTable()->definition();
+ $wherestt = 'WHERE';
+ $restrictions = $last->getRestrictions();
+ $orderstt = 'ORDER BY';
+ $order = $this->getOrder();
+ $groupbystt = 'GROUP BY';
+ $groupby = $this->aggregate;
+ $limitstt = 'LIMIT';
+ $limit = $offset . ', ' . $max;
+
+ if ($fields === null) {
+ $fields = $last->getQueryTable()->getFields();
+ }
+ else {
+ $fields = collect($fields)->each(function ($e) { return $e instanceof \spitfire\storage\database\AggregateFunction? sprintf('%s(%s) AS %s', $e->getOperation(), $e->getField(), $e->getAlias()) : $e; })->toArray();
+ }
+
+ if (!empty($this->calculated)) {
+ foreach ($this->calculated as $calculated) {
+ $fields[] = sprintf('%s(%s) AS %s', $calculated->getOperation(), $calculated->getField(), $calculated->getAlias());
+ }
+ }
+
+ $join = implode(' ', $joins);
+
+ #Restrictions
+ if (empty($restrictions)) {
+ $restrictions = '1';
+ }
+ else {
+ $restrictions = implode(' AND ', $restrictions);
+ }
+
+ if ($max === null) {
+ $limitstt = '';
+ $limit = '';
+ }
+
+ if (empty($order)) {
+ $orderstt = '';
+ $order = '';
+ }
+ else {
+ $field = $order['field'] instanceof \spitfire\storage\database\AggregateFunction? $order['field']->getAlias() : $order['field'];
+ $order = "{$field} {$order['mode']}";
+ }
+
+ if (empty($groupby)) {
+ $groupbystt = '';
+ $groupby = '';
+ }
+ else {
+ $groupby = implode(', ', $groupby);
+ }
+
+ $stt = array_filter(Array( $selectstt, implode(', ', $fields), $fromstt, $tablename, $join,
+ $wherestt, $restrictions, $groupbystt, $groupby, $orderstt, $order, $limitstt, $limit));
+
+ return new ResultSet($this->getTable(), $this->getTable()->getDb()->execute(implode(' ', $stt)));
+
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20171110
+ * @param QueryField $field
+ * @param type $value
+ * @param type $operator
+ * @return MysqlPDORestriction
+ */
+ public function restrictionInstance(QueryField$field, $value, $operator) {
+ return new MysqlPDORestriction($this, $field, $value, $operator);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20171110
+ * @param QueryField $field
+ * @return \spitfire\storage\database\drivers\MysqlPDOQueryField|QueryField
+ */
+ public function queryFieldInstance($field) {
+ trigger_error('Deprecated: mysqlPDOQuery::queryFieldInstance is deprecated', E_USER_DEPRECATED);
+
+ if ($field instanceof QueryField) {return $field; }
+ return new MysqlPDOQueryField($this, $field);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20171110
+ * @param type $table
+ * @return \spitfire\storage\database\drivers\MysqlPDOQueryTable
+ * @throws PrivateException
+ */
+ public function queryTableInstance($table) {
+ if ($table instanceof Relation) { $table = $table->getTable(); }
+ if ($table instanceof QueryTable) { $table = $table->getTable(); }
+
+
+ if (!$table instanceof Table) { throw new PrivateException('Did not receive a table as parameter'); }
+
+ return new MysqlPDOQueryTable($this, $table);
+ }
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20171110
+ */
+ public function compositeRestrictionInstance(Field $field = null, $value, $operator) {
+ return new CompositeRestriction($this, $field, $value, $operator);
+ }
+
+ /**
+ *
+ * @fixme
+ */
+ public function delete() {
+
+
+ $this->setAliased(false);
+
+ #Declare vars
+ $selectstt = 'DELETE';
+ $fromstt = 'FROM';
+ $tablename = $this->getTable();
+ $wherestt = 'WHERE';
+ /** @link http://www.spitfirephp.com/wiki/index.php/Database/subqueries Information about the filter*/
+ $restrictions = array_filter($this->getRestrictions(), Array('spitfire\storage\database\Query', 'restrictionFilter'));
+
+
+ #Import tables for restrictions from remote queries
+ $subqueries = $this->getPhysicalSubqueries();
+ $joins = Array();
+
+ foreach ($subqueries as $q) {
+ $joins[] = sprintf('LEFT JOIN %s ON (%s)', $q->getQueryTable()->definition(), implode(' AND ', $q->getRestrictions()));
+ }
+
+ $join = implode(' ', $joins);
+
+ #Restrictions
+ if (empty($restrictions)) {
+ $restrictions = '1';
+ }
+ else {
+ $restrictions = implode(' AND ', $restrictions);
+ }
+
+ $stt = array_filter(Array( $selectstt, $fromstt, $tablename, $join,
+ $wherestt, $restrictions));
+
+ $this->getTable()->getDb()->execute(implode(' ', $stt));
+ }
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/QueryField.php b/src/drivers/mysqlpdo/QueryField.php
new file mode 100644
index 0000000..369b1fb
--- /dev/null
+++ b/src/drivers/mysqlpdo/QueryField.php
@@ -0,0 +1,10 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\QueryField as ParentClass;
+
+class QueryField extends ParentClass
+{
+ public function __toString() {
+ return "{$this->getQueryTable()}.`{$this->getField()->getName()}`";
+ }
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/QueryTable.php b/src/drivers/mysqlpdo/QueryTable.php
new file mode 100644
index 0000000..0eb320f
--- /dev/null
+++ b/src/drivers/mysqlpdo/QueryTable.php
@@ -0,0 +1,24 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\QueryTable as ParentClass;
+
+class QueryTable extends ParentClass
+{
+ /**
+ *
+ * @todo Move the aliasing thing over to the queryTable completely.
+ * @return string
+ */
+ public function __toString() {
+ return "`{$this->getAlias()}`";
+ }
+
+ public function definition() {
+ if ($this->isAliased()) {
+ return "{$this->getTable()->getLayout()} AS `{$this->getAlias()}`";
+ }
+ else {
+ return "{$this->getTable()->getLayout()}";
+ }
+ }
+}
diff --git a/src/drivers/mysqlpdo/Relation.php b/src/drivers/mysqlpdo/Relation.php
new file mode 100644
index 0000000..c4d16d8
--- /dev/null
+++ b/src/drivers/mysqlpdo/Relation.php
@@ -0,0 +1,147 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+class Relation extends \spitfire\storage\database\Relation
+{
+
+
+ /**
+ * Deletes this record from the database. This method's call cannot be
+ * undone. <b>[NOTICE]</b>Usually Spitfire will cascade all the data
+ * related to this record. So be careful calling it deliberately.
+ *
+ * @param Model $record Database record to be deleted from the DB.
+ */
+ public function delete(\spitfire\Model$record) {
+ $table = $this->getTable();
+ $db = $table->getDb();
+ $key = $record->getPrimaryData();
+
+ $restrictions = Array();
+
+ foreach ($key as $k => $v) {
+ $restrictions[] = sprintf('%s = %s', $k, $db->quote($v));
+ }
+
+ $stt = sprintf('DELETE FROM `%s` WHERE %s',
+ $table->getLayout()->getTablename(),
+ implode(' AND ', $restrictions)
+ );
+ $db->execute($stt);
+ }
+
+ /**
+ * Modifies this record on high write environments. If two processes modify
+ * this record simultaneously they won't generate unconsistent data.
+ * This function is especially useful for counters i.e. pageviews, clicks,
+ * plays or any kind of transactions.
+ *
+ * @throws PrivateException If the database couldn't handle the request.
+ * @param Model $record Database record to be modified.
+ * @param string $key
+ * @param int|float|double $diff
+ */
+ public function increment(\spitfire\Model$record, $key, $diff = 1) {
+
+ $table = $this->getTable();
+ $db = $table->getDb();
+ $pk = $record->getPrimaryData();
+
+ $restrictions = Array();
+ foreach ($pk as $k => $v) {$restrictions[] = "$k = $v";}
+
+ $stt = sprintf('UPDATE %s SET `%s` = `%s` + %s WHERE %s',
+ $table->getLayout(),
+ $key,
+ $key,
+ $db->quote($diff),
+ implode(' AND ', $restrictions)
+ );
+
+ $db->execute($stt);
+ }
+
+ public function insert(\spitfire\Model$record) {
+ $data = $record->getData();
+ $table = $record->getTable();
+ $db = $table->getDb();
+
+ $write = Array();
+
+ foreach ($data as $value) {
+ $write = array_merge($write, $value->dbGetData());
+ }
+
+ $fields = array_keys($write);
+ foreach ($fields as &$field) { $field = '`' . $field . '`'; }
+ unset($field);
+
+ $quoted = array_map(Array($db, 'quote'), $write);
+
+ $stt = sprintf('INSERT INTO %s (%s) VALUES (%s)',
+ $table->getLayout(),
+ implode(', ', $fields),
+ implode(', ', $quoted)
+ );
+ $db->execute($stt);
+ return $db->getConnection()->lastInsertId();
+ }
+
+ public function update(\spitfire\Model$record) {
+ $data = $record->getData();
+ $table = $record->getTable();
+ $db = $table->getDb();
+ $key = $record->getPrimaryData();
+
+ $restrictions = Array();
+ foreach ($key as $k => $v) {$restrictions[] = "{$table->getLayout()->getField($k)} = {$db->quote($v)}";}
+
+ $write = Array();
+
+ foreach ($data as $value) {
+ #If the data we want to write to the database is already there and
+ #properly synced, we just skip it.
+ if ($value->isSynced()) { continue; }
+
+ $write = array_merge($write, $value->dbGetData());
+ }
+
+ if (empty($write)) { return; }
+
+ $quoted = Array();
+ foreach ($write as $f => $v) { $quoted[] = "{$table->getLayout()->getField($f)} = {$db->quote($v)}"; }
+
+ $stt = sprintf('UPDATE %s SET %s WHERE %s',
+ $table->getLayout(),
+ implode(', ', $quoted),
+ implode(' AND ', $restrictions)
+ );
+
+ $this->getDb()->execute($stt);
+
+ }
+
+}
diff --git a/src/drivers/mysqlpdo/Restriction.php b/src/drivers/mysqlpdo/Restriction.php
new file mode 100644
index 0000000..bdfca38
--- /dev/null
+++ b/src/drivers/mysqlpdo/Restriction.php
@@ -0,0 +1,32 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\Restriction as AbstractRestriction;
+
+class Restriction extends AbstractRestriction
+{
+ public function __toString() {
+ $value = $this->getValue();
+
+ if (is_array($value)) {
+ foreach ($value as &$v) {
+ $v = $this->getTable()->getDb()->quote($v);
+ }
+
+ $quoted = implode(',', $value);
+ return "{$this->getField()} {$this->getOperator()} ({$quoted})";
+ }
+
+ elseif ($value instanceof QueryField) {
+ return "{$this->getField()} {$this->getOperator()} {$this->getValue()}";
+ }
+ elseif ($value === null) {
+ $operator = in_array($this->getOperator(), ['IS', '='])? 'IS' : 'IS NOT';
+ return "{$this->getField()} {$operator} NULL";
+ }
+ else {
+ $quoted = $this->getTable()->getDb()->quote($value);
+ return "{$this->getField()} {$this->getOperator()} {$quoted}";
+ }
+ }
+
+}
diff --git a/src/drivers/mysqlpdo/RestrictionGroup.php b/src/drivers/mysqlpdo/RestrictionGroup.php
new file mode 100644
index 0000000..71c96e8
--- /dev/null
+++ b/src/drivers/mysqlpdo/RestrictionGroup.php
@@ -0,0 +1,11 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\drivers\sql\SQLRestrictionGroup;
+
+class RestrictionGroup extends SQLRestrictionGroup
+{
+ public function __toString() {
+ if ($this->isEmpty()) { return ''; }
+ return sprintf('(%s)', implode(' ' . $this->getType() .' ', $this->getRestrictions()));
+ }
+}
\ No newline at end of file
diff --git a/src/drivers/mysqlpdo/ResultSet.php b/src/drivers/mysqlpdo/ResultSet.php
new file mode 100644
index 0000000..e16b60b
--- /dev/null
+++ b/src/drivers/mysqlpdo/ResultSet.php
@@ -0,0 +1,71 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use PDO;
+use PDOStatement;
+use spitfire\core\Collection;
+use spitfire\storage\database\ResultSetInterface;
+use spitfire\storage\database\Table;
+
+/**
+ * This class works as a traditional resultset. It acts as an adapter between the
+ * driver's raw data retrieving and the logical record classes.
+ *
+ * @author César de la Cal <cesar@magic3w.com>
+ */
+class ResultSet implements ResultSetInterface
+{
+ /**
+ * Contains the raw pointer that PDO has created when executing the query.
+ * This allows spitfire to retrieve all the data needed to create a complete
+ * database record.
+ *
+ * @var PDOStatement
+ */
+ private $result;
+
+ /**
+ * This is a reference to the table this resultset belongs to. This allows
+ * Spitfire to retrieve data about the model and the fields the datatype has.
+ *
+ * @var Table
+ */
+ private $table;
+
+ public function __construct(Table$table, $stt) {
+ $this->result = $stt;
+ $this->table = $table;
+ }
+
+ public function fetch() {
+ $data = $this->result->fetch(PDO::FETCH_ASSOC);
+ #If the data does not contain anything we return a null object
+ if (!$data) { return null; }
+ $_record = array_map( Array($this->table->getDB()->getEncoder(), 'decode'), $data);
+
+ $record = $this->table->newRecord($_record);
+ return $record;
+ }
+
+ public function fetchAll() {
+ $data = $this->result->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($data as &$record) {
+ $record = $this->table->newRecord(
+ array_map( Array($this->table->getDB()->getEncoder(), 'decode'), $record)
+ );
+ }
+
+ return new Collection($data);
+ }
+
+ /**
+ * Returns the data the way any associative adapter would return it. This allows
+ * your app to withdraw raw data without it being treated by the framework.
+ *
+ * @return mixed
+ */
+ public function fetchArray() {
+ return $this->result->fetch(PDO::FETCH_ASSOC);
+ }
+
+}
diff --git a/src/drivers/sql/SQLQuery.php b/src/drivers/sql/SQLQuery.php
new file mode 100644
index 0000000..aebd870
--- /dev/null
+++ b/src/drivers/sql/SQLQuery.php
@@ -0,0 +1,178 @@
+<?php namespace spitfire\storage\database\drivers\sql;
+
+use spitfire\core\Collection;
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\drivers\mysqlpdo\CompositeRestriction;
+use spitfire\storage\database\Query;
+use spitfire\storage\database\RestrictionGroup;
+
+abstract class SQLQuery extends Query
+{
+
+ /**
+ * The redirection object is required only when assembling queries. Sometimes,
+ * a query has unmet dependencies that it cannot satisfy. In this case, it's
+ * gonna copy itself and move all of it's restrictions to the new query.
+ *
+ * This means that when serializing the query, the composite restriction should
+ * not print <code>old.primary IS NOT NULL</code> but <code>new.primary IS NOT NULL</code>.
+ *
+ * But! When the parent injects the restrictions to connect the queries with
+ * the parent, the old query must answer the call and assimilate them.
+ *
+ * To achieve this behavior, I found it reasonable that the query introduces
+ * a redirection property. When a composite restriction finds this, it will
+ * automatically use the target of the redirection.
+ *
+ * NOTE: Composite queries do not follow multiple redirections.
+ *
+ * @var SQLQuery|null
+ */
+ private $redirection = null;
+
+ /**
+ * It retrieves all the subqueries that are needed to be executed on a relational
+ * DB before the main query.
+ *
+ * We could have used a single method with a flag, but this way seems cleaner
+ * and more hassle free than otherwise.
+ *
+ * @return Query[]
+ */
+ public function makeExecutionPlan() {
+
+ /*
+ * Inject the current query into the array. The data for this query needs
+ * to be retrieved last.
+ */
+ $copy = clone $this;
+ $_ret = $copy->physicalize(true);
+
+ $copy->denormalize(true);
+
+ foreach ($_ret as $q) {
+ $q->normalize();
+ }
+
+ return $_ret;
+ }
+
+ public function physicalize($top = false) {
+
+ $copy = $this;
+ $_ret = [$this];
+
+ $composite = $copy->getCompositeRestrictions();
+
+ foreach ($composite as $r) {
+
+ $q = $r->getValue();
+ $p = $q->physicalize();
+ $c = $r->makeConnector();
+ $_ret = array_merge($_ret, $c, $p);
+ }
+
+ if (!$top && $copy->isMixed() && !$composite->isEmpty()) {
+
+ $clone = clone $copy;
+ $of = $copy->getTable()->getDb()->getObjectFactory();
+
+ $clone->cloneQueryTable();
+ $group = $of->restrictionGroupInstance($clone);
+
+ foreach ($copy->getTable()->getPrimaryKey()->getFields() as $field) {
+ $group->where(
+ $of->queryFieldInstance($copy->getQueryTable(), $field),
+ $of->queryFieldInstance($clone->getQueryTable(), $field)
+ );
+ }
+
+ $copy->reset();
+ $copy->setRedirection($clone);
+ $clone->push($group);
+ $_ret[] = $clone;
+ }
+
+ return $_ret;
+ }
+
+ /**
+ *
+ * @todo This method could be much simpler with an import function in rGroups which take all the children
+ * @param type $root
+ * @return Collection
+ * @throws PrivateException
+ */
+ public function denormalize($root = false) {
+ /*
+ * Previously we had a condition here that would lead to an exception.
+ * Replacing this with an assertion makes it more clear that this code is
+ * not an error handler but describes an impossible condition that should
+ * not be found.
+ */
+ assert($root || !$this->isMixed() || $this->getCompositeRestrictions()->isEmpty());
+
+ $_ret = new Collection();
+
+ $composite = $this->getCompositeRestrictions();
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
+
+ /*
+ * Loop over the composite restrictions inside this query. This allows the
+ * system to extract the conditions that need to assimilated at the end of
+ * a query.
+ *
+ * Please note that this only can be achieved because the driver has
+ * previously physicalized the queries and prepared them in a manner that
+ * allows for their denormalization (deferred the mixed ones).
+ */
+ foreach ($composite as /*@var $r CompositeRestriction*/$r) {
+
+ $sg = $of->restrictionGroupInstance($r->getParent(), RestrictionGroup::TYPE_AND);
+ $d = $r->getValue()->denormalize();
+
+ /*
+ * Once the subquery has been denormalized, we enter to retrieve the
+ * previously denormalized blocks. To do so, we loop over the collection
+ * containing the references, remove the group from their actual location,
+ * put it in their new home and append that to the $sg variable.
+ */
+ foreach ($d as $v) {
+ $group = $of->restrictionGroupInstance($this, RestrictionGroup::TYPE_AND);
+ $group->push($v);
+ $v->getParent()->remove($v);
+ $v->setParent($group);
+ $sg->push($group->setParent($sg));
+ }
+
+ $sg->push($r);
+
+ $r->getParent()->remove($r)->push($sg);
+ $r->setParent($sg);
+ $_ret->push($sg);
+ }
+
+ return $_ret;
+ }
+
+ /**
+ *
+ * @return SQLQuery|null
+ */
+ public function getRedirection() {
+ return $this->redirection;
+ }
+
+ /**
+ * This is a driver specific method. If you're not exactly sure what a query
+ * redirection is, please avoid using this method.
+ *
+ * @param SQLQuery|null $redirection
+ * @return $this
+ */
+ public function setRedirection($redirection = null) {
+ $this->redirection = $redirection;
+ return $this;
+ }
+
+}
diff --git a/src/drivers/sql/SQLRestrictionGroup.php b/src/drivers/sql/SQLRestrictionGroup.php
new file mode 100644
index 0000000..3a2c094
--- /dev/null
+++ b/src/drivers/sql/SQLRestrictionGroup.php
@@ -0,0 +1,26 @@
+<?php namespace spitfire\storage\database\drivers\sql;
+
+use spitfire\storage\database\CompositeRestriction;
+use spitfire\storage\database\RestrictionGroup;
+
+
+abstract class SQLRestrictionGroup extends RestrictionGroup
+{