├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── INSTALL.md ├── LICENSE.md ├── Makefile ├── README.md ├── TODO.md ├── apigen.neon ├── composer.json ├── docs ├── .gitignore ├── Makefile ├── _exts │ └── sensio │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __init__.py │ │ └── sphinx │ │ ├── __init__.py │ │ ├── configurationblock.py │ │ ├── php.py │ │ ├── phpcode.py │ │ └── refinclude.py ├── _static │ └── .placeholder ├── _templates │ └── .placeholder ├── conf.py ├── configure.rst ├── events.rst ├── images │ ├── relation-composite.graphml │ ├── relation-composite.jpg │ ├── relation.graphml │ └── relation.jpg ├── index.rst ├── install.rst ├── make.bat ├── relations.rst ├── requirements.txt ├── setup.rst ├── transactions.rst └── usage.rst ├── example ├── README.md ├── config.json ├── events.php ├── example.sq3 ├── models-read.php ├── models-write.php ├── models │ ├── Contact.php │ ├── Person.php │ ├── Post.php │ └── Tag.php ├── querysets-read.php ├── querysets-write.php ├── relations.php └── setup.sql ├── phpunit.xml.dist ├── src └── Phormium │ ├── Autoloader.php │ ├── Config │ ├── ArrayLoader.php │ ├── Configuration.php │ ├── FileLoader.php │ ├── JsonLoader.php │ ├── PostProcessor.php │ └── YamlLoader.php │ ├── Container.php │ ├── DB.php │ ├── Database │ ├── Connection.php │ ├── Database.php │ ├── Driver.php │ └── Factory.php │ ├── Event.php │ ├── Exception │ ├── ConfigurationException.php │ ├── DatabaseException.php │ ├── InvalidModelException.php │ ├── InvalidQueryException.php │ ├── InvalidRelationException.php │ ├── ModelNotFoundException.php │ └── OrmException.php │ ├── Filter │ ├── ColumnFilter.php │ ├── CompositeFilter.php │ ├── Filter.php │ └── RawFilter.php │ ├── Helper │ ├── Assert.php │ └── Json.php │ ├── Meta.php │ ├── MetaBuilder.php │ ├── Model.php │ ├── ModelRelationsTrait.php │ ├── Orm.php │ ├── Printer.php │ ├── Query.php │ ├── Query │ ├── Aggregate.php │ ├── ColumnOrder.php │ ├── LimitOffset.php │ ├── OrderBy.php │ └── QuerySegment.php │ ├── QueryBuilder │ ├── Common │ │ ├── FilterRenderer.php │ │ ├── QueryBuilder.php │ │ └── Quoter.php │ ├── Mysql │ │ ├── QueryBuilder.php │ │ └── Quoter.php │ ├── Pgsql │ │ └── QueryBuilder.php │ ├── QueryBuilderFactory.php │ └── QueryBuilderInterface.php │ └── QuerySet.php └── tests ├── bootstrap.php ├── config.json ├── integration ├── AggregateTest.php ├── ConnectionTest.php ├── DbTest.php ├── FilterTest.php ├── ModelRelationsTraitTest.php ├── ModelTest.php ├── PrinterTest.php ├── QuerySetTest.php └── TransactionTest.php ├── models ├── Asset.php ├── Contact.php ├── InvalidModel1.php ├── InvalidModel2.php ├── Model1.php ├── Model2.php ├── NotModel.php ├── Person.php ├── PkLess.php └── Trade.php ├── performance ├── README.md ├── functions.php ├── model.php ├── performance.php ├── results │ └── .gitignore └── world.sql ├── travis ├── before.sh ├── bootstrap.php ├── mysql │ ├── config.json │ ├── phpunit.xml │ └── setup.sql ├── postgres │ ├── config.json │ ├── phpunit.xml │ └── setup.sql └── sqlite │ ├── config.json │ ├── phpunit.xml │ └── setup.sql └── unit ├── Config ├── ConfigurationTest.php ├── LoaderTest.php └── PostProcessorTest.php ├── Database ├── ConnectionTest.php ├── DatabaseTest.php └── FactoryTest.php ├── Filter ├── ColumnFiterTest.php ├── CompositeFilterTest.php ├── FilterTest.php └── RawFilterTest.php ├── Helper └── AssertTest.php ├── MetaBuilderTest.php ├── Query ├── AggregateTest.php ├── ColumnOrderTest.php ├── LimitOffsetTest.php ├── OrderByTest.php └── QuerySegmentTest.php └── QueryBuilder ├── ColumnFilterRendererTest.php ├── CompositeFilterRendererTest.php ├── QueryBuilderTest.php └── RawFilterRendererTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Use LF for line endings 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | vendor 3 | tmp 4 | php_errors.log 5 | composer.phar 6 | composer.lock 7 | /.project 8 | /.settings 9 | /phpunit.xml 10 | *.sublime-project 11 | *.sublime-workspace 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '5.6' 5 | - '7.0' 6 | - '7.1' 7 | - hhvm 8 | 9 | env: 10 | - DB=mysql 11 | - DB=sqlite 12 | - DB=postgres 13 | 14 | matrix: 15 | # hhvm doesn't support postgres currently 16 | allow_failures: 17 | - php: hhvm 18 | env: DB=postgres 19 | fast_finish: true 20 | 21 | before_script: sh tests/travis/before.sh 22 | 23 | script: phpunit --configuration tests/travis/$DB/phpunit.xml 24 | 25 | sudo: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Phormium Changelog 2 | ================== 3 | 4 | 0.9.0 / TBA 5 | ----------- 6 | 7 | This release reorganizes the code substantially. The global state which was 8 | littered all over the project (DB, Conf, Event) is now consolidated in one 9 | place, the central `Phormium\Orm` class. 10 | 11 | New features: 12 | 13 | * `QuerySet`s are now iterable - this will fetch rows one by one instead of 14 | fetching all at once. 15 | * Column and table names are properly quoted in SQL as expected by each 16 | database - double quotes for most and backticks for MySQL. 17 | 18 | This causes several breaks to **backward compatibility**: 19 | 20 | * **Requires PHP 5.6** 21 | * `DB` class is deprecated in favour of `Orm::database()` 22 | * `Event` class is deprecated in favour of `Orm::emitter()` 23 | * `Conf` class has been removed, use `Orm::configure()` 24 | * Made `Printer` methods non-static 25 | * Made `ColumnFilter`, `RawFilter` and `CompositeFilter` immutable. 26 | * `CompositeFilter::add()` no longer exists because it mutated the filter, and 27 | has been replaced with `CompositeFilter::withAdded()` which returns a new 28 | `CompositeFilter` instance. 29 | * Made `ColumnFilter`, `RawFilter` and `CompositeFilter` properties private, 30 | available via getter methods which are named the same as the properties, e.g. 31 | `ColumnFilter::value()` 32 | * Renamed existing `CompositeFilter` accessor methods: 33 | * `CompositeFilter::getOperation()` -> `CompositeFilter::operation()` 34 | * `CompositeFilter::getFilters()` -> `CompositeFilter::filters()` 35 | * Several changes to `Aggregate` 36 | * Moved to `Phormium\Query` namespace 37 | * Made properties private, available via getter methods. 38 | 39 | Deprecated methods will emit a deprecation warning when used and will be removed 40 | in the next release. 41 | 42 | Bug fixes: 43 | 44 | * Fix: `Connection->execute()` should return the number of affected rows (#12) 45 | * Fix: Apply limit and offset to distinct queries (#18) 46 | * Fix: Fix handling of boolean `false` (#24) 47 | 48 | 0.8.0 / 2015-05-07 49 | ------------------ 50 | 51 | * Added database attributes to configuration 52 | * **BC BREAK**: Phormium will no longer force lowercase column names on 53 | database tables. This can still be done manually by setting the 54 | `PDO::ATTR_CASE:` attribute to `PDO::CASE_LOWER` in the configuration. 55 | 56 | 0.7.0 / 2014-12-05 57 | ------------------ 58 | 59 | * **BC BREAK**: Dropped support for PHP 5.3 60 | * Added a shorthand for model relations with `Model->hasChildren()` and 61 | `Model->hasParent()` 62 | 63 | 0.6.2 / 2014-09-28 64 | ------------------ 65 | 66 | * Fixed an issue with shallow cloning which caused the same Filter instance to 67 | be used in cloned QuerySets. 68 | 69 | 0.6.1 / 2014-09-13 70 | ------------------ 71 | 72 | * Added `DB::disconnect()`, for disconnecting a single connection 73 | * Added `DB::isConnected()`, for checking if a connection is up 74 | * Added `DB::setConnection()`, useful for mocking 75 | * Added `Connection->inTransaction()` 76 | 77 | 0.6 / 2014-04-10 78 | ---------------- 79 | 80 | * **BC BREAK**: Moved filter classes to `Phormium\Filter` namespace
81 | Please update your references (e.g. `use Phormium\ColumnFilter` to 82 | `use Phormium\Filter\ColumnFilter`). 83 | * **BC BREAK**: Removed logging and stats classes
84 | These will be reimplemented using events and available as separate packages. 85 | * Added `Model::all()` 86 | * Added `Model->toYAML()` 87 | * Added `Model::fromYAML()` 88 | * Added raw filters 89 | * Added events 90 | 91 | * Modified `Model::fromJSON()` to take an optional `$strict` parameter 92 | 93 | 0.5 / 2013-12-10 94 | ---------------- 95 | 96 | * Added `Model->dump()` 97 | * Added `Filter::col()` 98 | * Added gathering of query stats 99 | 100 | 0.4 / 2013-07-17 101 | ---------------- 102 | 103 | * Added support for custom queries via `Connection` object 104 | * Added `Model->merge()` 105 | * Added `Model::find()` 106 | * Added `Model::exists()` 107 | * Modified `Model::get()` to accept the primary key as an array 108 | * Modified `Model->save()` to be safer 109 | 110 | 0.3 / 2013-06-14 111 | ---------------- 112 | 113 | * Added `QuerySet::valuesFlat()` 114 | * Added optional parameter `$allowEmpty` to `QuerySet->single()` 115 | 116 | 0.2 / 2013-05-10 117 | ---------------- 118 | 119 | * Added transactions 120 | * Added `QuerySet->dump()` 121 | * Added logging via [Apache log4php](http://logging.apache.org/log4php/) 122 | * Added composite filters 123 | 124 | 0.1 / 2013-04-25 125 | ---------------- 126 | 127 | * Initial release 128 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Install 2 | ======= 3 | 4 | Using Composer 5 | -------------- 6 | Install [Composer](http://getcomposer.org/download/). 7 | 8 | Create a `composer.json` file: 9 | 10 | ```javascript 11 | { 12 | "require": { 13 | "ihabunek/phormium": "dev-master" 14 | } 15 | } 16 | ``` 17 | 18 | Start the Composer install procedure: 19 | 20 | php composer.phar install 21 | 22 | Phormium will be installed in `vendor/phormium/phormium`. 23 | 24 | In your code, include `vendor/autoload.php` to get access to Phormium classes. 25 | 26 | ```php 27 | require 'vendor/autoload.php'; 28 | ``` 29 | 30 | Clone from Github 31 | ----------------- 32 | 33 | Clone the source into `phormium` directory: 34 | 35 | git clone https://github.com/ihabunek/phormium.git 36 | 37 | In your code, include and register the Phormium autoloader: 38 | 39 | ```php 40 | require 'phormium/Phormium/Autoloader.php'; 41 | \Phormium\Autoloader::register(); 42 | ``` 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Phormium License 2 | ================ 3 | 4 | Copyright (c) 2012 Ivan Habunek 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | sqlite : 2 | phpunit -c tests/travis/sqlite/phpunit.xml 3 | 4 | mysql : 5 | phpunit -c tests/travis/mysql/phpunit.xml 6 | 7 | postgres : 8 | phpunit -c tests/travis/postgres/phpunit.xml 9 | 10 | all : sqlite mysql postgres 11 | 12 | coverage : 13 | phpunit -c tests/travis/sqlite/phpunit.xml --coverage-html target/coverage --whitelist src 14 | 15 | unit-coverage : 16 | phpunit -c tests/travis/sqlite/phpunit.xml --coverage-html target/coverage --whitelist src --testsuite unit 17 | 18 | documentation : 19 | cd docs && make clean && make html 20 | 21 | clean : 22 | rm -rf target/coverage 23 | 24 | default: sqlite 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Phormium 2 | ======== 3 | 4 | Phormium is a minimalist ORM for PHP. 5 | 6 | Tested on Informix, MySQL, PosgreSQL and SQLite. Might work on other databases 7 | with a PDO driver or may require some work. 8 | 9 | --- 10 | 11 | **This project is no longer maintained and the repository is archived. Thanks for all the fish.** 12 | 13 | --- 14 | 15 | [![Latest Stable Version](https://poser.pugx.org/phormium/phormium/v/stable.png)](https://packagist.org/packages/phormium/phormium) [![Total Downloads](https://poser.pugx.org/phormium/phormium/downloads.png)](https://packagist.org/packages/phormium/phormium) [![Build Status](https://travis-ci.org/ihabunek/phormium.png)](https://travis-ci.org/ihabunek/phormium) [![Coverage Status](https://coveralls.io/repos/ihabunek/phormium/badge.png)](https://coveralls.io/r/ihabunek/phormium) 16 | 17 | Features 18 | -------- 19 | 20 | * CRUD operations made simple 21 | * batch update and delete 22 | * filtering 23 | * ordering 24 | * limiting 25 | * transactions 26 | * custom queries 27 | * events 28 | 29 | Documentation 30 | ------------- 31 | 32 | [The documentation](http://phormium.readthedocs.org/en/latest/) is hosted by 33 | ReadTheDocs.org. 34 | 35 | Showcase 36 | -------- 37 | 38 | After initial setup, Phormium is very easy to use. Here's a quick overview of 39 | it's features: 40 | 41 | ```php 42 | // Create a new person record 43 | $person = new Person(); 44 | $person->name = "Frank Zappa"; 45 | $person->birthday = "1940-12-21"; 46 | $person->save(); 47 | 48 | // Get record by primary key 49 | Person::get(10); // Throws exception if the model doesn't exist 50 | Person::find(10); // Returns null if the model doesn't exist 51 | 52 | // Check record exists by primary key 53 | Person::exists(10); 54 | 55 | // Also works for composite primary keys 56 | Post::get('2013-01-01', 100); 57 | Post::find('2013-01-01', 100); 58 | Post::exists('2013-01-01', 100); 59 | 60 | // Primary keys can also be given as arrays 61 | Post::get(['2013-01-01', 100]); 62 | Post::find(['2013-01-01', 100]); 63 | Post::exists(['2013-01-01', 100]); 64 | 65 | // Fetch, update, save 66 | $person = Person::get(10); 67 | $person->salary += 5000; // give the man a raise! 68 | $person->save(); 69 | 70 | // Fetch, delete 71 | Person::get(37)->delete(); 72 | 73 | // Intuitive filtering, ordering and limiting 74 | $persons = Person::objects() 75 | ->filter('salary', '>', 10000) 76 | ->filter('birthday', 'between', ['2000-01-01', '2001-01-01']) 77 | ->orderBy('name', 'desc') 78 | ->limit(100) 79 | ->fetch(); 80 | 81 | // Count records 82 | $count = Person::objects() 83 | ->filter('salary', '>', 10000) 84 | ->count(); 85 | 86 | // Check if any records matching criteria exist 87 | $count = Person::objects() 88 | ->filter('salary', '>', 10000) 89 | ->exists(); 90 | 91 | // Distinct values 92 | $count = Person::objects() 93 | ->distinct('name', 'email'); 94 | 95 | // Complex composite filters 96 | $persons = Person::objects()->filter( 97 | Filter::_or( 98 | Filter::_and( 99 | array('id', '>=', 10), 100 | array('id', '<=', 20) 101 | ), 102 | Filter::_and( 103 | array('id', '>=', 50), 104 | array('id', '<=', 60) 105 | ), 106 | array('id', '>=', 100), 107 | ) 108 | )->fetch(); 109 | 110 | // Fetch a single record (otherwise throws an exeption) 111 | $person = Person::objects() 112 | ->filter('email', '=', 'ivan@example.com') 113 | ->single(); 114 | 115 | // Batch update 116 | Person::objects() 117 | ->filter('salary', '>', 10000) 118 | ->update(['salary' => 5000]); 119 | 120 | // Batch delete 121 | Person::objects() 122 | ->filter('salary', '>', 10000) 123 | ->delete(); 124 | 125 | // Aggregates 126 | Person::objects()->filter('name', 'like', 'Ivan%')->avg('salary'); 127 | Person::objects()->filter('name', 'like', 'Marko%')->min('birthday'); 128 | 129 | // Custom filters with argument binding 130 | Person::objects() 131 | ->filter("my_func(salary) > ?", [100]) 132 | ->fetch(); 133 | ``` 134 | 135 | See [documentation](http://phormium.readthedocs.org/en/latest/) for full 136 | reference, also check out the `example` directory for more examples. 137 | 138 | Why? 139 | ---- 140 | 141 | "Why another ORM?!?", I hear you cry. 142 | 143 | There are two reasons: 144 | 145 | * I work a lot on Informix on my day job and no other ORM I found supports it. 146 | * Writing an ORM is a great experience. You should try it. 147 | 148 | Phormium is greatly inspired by other ORMs, in particular: 149 | 150 | * [Django ORM](https://docs.djangoproject.com/en/dev/topics/db/) 151 | * Laravel's [Eloquent ORM](http://laravel.com/docs/database/eloquent) 152 | * [Paris](http://j4mie.github.io/idiormandparis/) 153 | 154 | Let me know what you think! 155 | 156 | Ivan Habunek [@ihabunek](http://twitter.com/ihabunek) 157 | 158 | Praise 159 | ------ 160 | 161 | If you like it, buy me a beer (in Croatia, that's around €2 or $3). 162 | 163 | [![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](http://flattr.com/thing/1204532/ihabunekphormium-on-GitHub) 164 | 165 | License 166 | ------- 167 | Licensed under the MIT license. `See LICENSE.md`. 168 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | Some ideas for the future. 5 | 6 | Get SQL which will be executed 7 | ------------------------------ 8 | 9 | Something like: 10 | 11 | ``` 12 | $qs->fetch(); // Executes 13 | $qs->fetchSQL(); // Just returns SQL 14 | ``` 15 | 16 | ``` 17 | $qs->single(); 18 | $qs->singleSQL(); 19 | ``` 20 | 21 | etc... 22 | 23 | 24 | It would return something like: 25 | ``` 26 | [ 27 | "SELECT ... FROM table WHERE field = :value", 28 | [ 29 | "value" => 1 30 | ] 31 | ] 32 | ``` 33 | 34 | Enable custom FK guessers 35 | ------------------------- 36 | 37 | Enable setting a custom ModelRelationsTrait::guessForeignKey() function. 38 | 39 | This will enable custome db naming schemas to be guessed by phormium, instead 40 | of having to be set manually. 41 | -------------------------------------------------------------------------------- /apigen.neon: -------------------------------------------------------------------------------- 1 | source: src 2 | destination: target/apidocs 3 | charset: UTF-8 4 | title: Phormium 5 | php: no 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phormium/phormium", 3 | "type": "library", 4 | "description": "A minimalist ORM for PHP.", 5 | "keywords": ["database","orm","php","mapping"], 6 | "homepage": "https://github.com/ihabunek/phormium", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Ivan Habunek", 11 | "email": "ivan@habunek.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.6.0", 16 | "ext-pdo": "*", 17 | "ext-mbstring": "*", 18 | "evenement/evenement": "^2.0.0", 19 | "pimple/pimple": "^3.0.0", 20 | "symfony/config": ">=2.3.0 <4.0.0", 21 | "symfony/yaml": ">=2.0.4 <4.0.0" 22 | }, 23 | "require-dev": { 24 | "mockery/mockery": "@stable" 25 | }, 26 | "autoload": { 27 | "psr-0": {"Phormium": "src/"} 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Phormium\\Tests\\Integration\\": "tests/integration/", 32 | "Phormium\\Tests\\Models\\": "tests/models/", 33 | "Phormium\\Tests\\Unit\\": "tests/unit/" 34 | } 35 | }, 36 | "support": { 37 | "issues": "https://github.com/ihabunek/phormium/issues" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | _build 3 | _env 4 | -------------------------------------------------------------------------------- /docs/_exts/sensio/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2013 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/_exts/sensio/README.md: -------------------------------------------------------------------------------- 1 | Sphinx Extensions for PHP and Symfony 2 | ===================================== 3 | 4 | After adding `sensio` to your path (with something like `sys.path.insert(0, 5 | os.path.abspath('./path/to/sensio'))`), you can use the following extensions 6 | in your `conf.py` file: 7 | 8 | * `sensio.sphinx.refinclude` 9 | * `sensio.sphinx.configurationblock` 10 | * `sensio.sphinx.phpcode` 11 | 12 | To enable highlighting for PHP code not between `` by default: 13 | 14 | lexers['php'] = PhpLexer(startinline=True) 15 | lexers['php-annotations'] = PhpLexer(startinline=True) 16 | 17 | And here is how to use PHP as the primary domain: 18 | 19 | primary_domain = 'php' 20 | 21 | Configure the `api_url` for links to the API: 22 | 23 | api_url = 'http://api.symfony.com/master/%s' 24 | -------------------------------------------------------------------------------- /docs/_exts/sensio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/_exts/sensio/__init__.py -------------------------------------------------------------------------------- /docs/_exts/sensio/sphinx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/_exts/sensio/sphinx/__init__.py -------------------------------------------------------------------------------- /docs/_exts/sensio/sphinx/configurationblock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | :copyright: (c) 2010-2012 Fabien Potencier 4 | :license: MIT, see LICENSE for more details. 5 | """ 6 | 7 | from docutils.parsers.rst import Directive, directives 8 | from docutils import nodes 9 | from string import upper 10 | 11 | class configurationblock(nodes.General, nodes.Element): 12 | pass 13 | 14 | class ConfigurationBlock(Directive): 15 | has_content = True 16 | required_arguments = 0 17 | optional_arguments = 0 18 | final_argument_whitespace = True 19 | option_spec = {} 20 | formats = { 21 | 'html': 'HTML', 22 | 'xml': 'XML', 23 | 'php': 'PHP', 24 | 'yaml': 'YAML', 25 | 'jinja': 'Twig', 26 | 'html+jinja': 'Twig', 27 | 'jinja+html': 'Twig', 28 | 'php+html': 'PHP', 29 | 'html+php': 'PHP', 30 | 'ini': 'INI', 31 | 'php-annotations': 'Annotations', 32 | } 33 | 34 | def run(self): 35 | env = self.state.document.settings.env 36 | 37 | node = nodes.Element() 38 | node.document = self.state.document 39 | self.state.nested_parse(self.content, self.content_offset, node) 40 | 41 | entries = [] 42 | for i, child in enumerate(node): 43 | if isinstance(child, nodes.literal_block): 44 | # add a title (the language name) before each block 45 | #targetid = "configuration-block-%d" % env.new_serialno('configuration-block') 46 | #targetnode = nodes.target('', '', ids=[targetid]) 47 | #targetnode.append(child) 48 | 49 | innernode = nodes.emphasis(self.formats[child['language']], self.formats[child['language']]) 50 | 51 | para = nodes.paragraph() 52 | para += [innernode, child] 53 | 54 | entry = nodes.list_item('') 55 | entry.append(para) 56 | entries.append(entry) 57 | 58 | resultnode = configurationblock() 59 | resultnode.append(nodes.bullet_list('', *entries)) 60 | 61 | return [resultnode] 62 | 63 | def visit_configurationblock_html(self, node): 64 | self.body.append(self.starttag(node, 'div', CLASS='configuration-block')) 65 | 66 | def depart_configurationblock_html(self, node): 67 | self.body.append('\n') 68 | 69 | def visit_configurationblock_latex(self, node): 70 | pass 71 | 72 | def depart_configurationblock_latex(self, node): 73 | pass 74 | 75 | def setup(app): 76 | app.add_node(configurationblock, 77 | html=(visit_configurationblock_html, depart_configurationblock_html), 78 | latex=(visit_configurationblock_latex, depart_configurationblock_latex)) 79 | app.add_directive('configuration-block', ConfigurationBlock) 80 | -------------------------------------------------------------------------------- /docs/_exts/sensio/sphinx/php.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | :copyright: (c) 2010-2012 Fabien Potencier 4 | :license: MIT, see LICENSE for more details. 5 | """ 6 | 7 | from sphinx import addnodes 8 | from sphinx.domains import Domain, ObjType 9 | from sphinx.locale import l_, _ 10 | from sphinx.directives import ObjectDescription 11 | from sphinx.domains.python import py_paramlist_re as js_paramlist_re 12 | from sphinx.roles import XRefRole 13 | from sphinx.util.nodes import make_refnode 14 | from sphinx.util.docfields import Field, GroupedField, TypedField 15 | 16 | def setup(app): 17 | app.add_domain(PHPDomain) 18 | 19 | class PHPXRefRole(XRefRole): 20 | def process_link(self, env, refnode, has_explicit_title, title, target): 21 | # basically what sphinx.domains.python.PyXRefRole does 22 | refnode['php:object'] = env.temp_data.get('php:object') 23 | if not has_explicit_title: 24 | title = title.lstrip('\\') 25 | target = target.lstrip('~') 26 | if title[0:1] == '~': 27 | title = title[1:] 28 | ns = title.rfind('\\') 29 | if ns != -1: 30 | title = title[ns+1:] 31 | if target[0:1] == '\\': 32 | target = target[1:] 33 | refnode['refspecific'] = True 34 | return title, target 35 | 36 | class PHPDomain(Domain): 37 | """PHP language domain.""" 38 | name = 'php' 39 | label = 'PHP' 40 | # if you add a new object type make sure to edit JSObject.get_index_string 41 | object_types = { 42 | } 43 | directives = { 44 | } 45 | roles = { 46 | 'func': PHPXRefRole(fix_parens=True), 47 | 'class': PHPXRefRole(), 48 | 'data': PHPXRefRole(), 49 | 'attr': PHPXRefRole(), 50 | } 51 | initial_data = { 52 | 'objects': {}, # fullname -> docname, objtype 53 | } 54 | 55 | def clear_doc(self, docname): 56 | for fullname, (fn, _) in self.data['objects'].items(): 57 | if fn == docname: 58 | del self.data['objects'][fullname] 59 | 60 | def find_obj(self, env, obj, name, typ, searchorder=0): 61 | if name[-2:] == '()': 62 | name = name[:-2] 63 | objects = self.data['objects'] 64 | newname = None 65 | if searchorder == 1: 66 | if obj and obj + '\\' + name in objects: 67 | newname = obj + '\\' + name 68 | else: 69 | newname = name 70 | else: 71 | if name in objects: 72 | newname = name 73 | elif obj and obj + '\\' + name in objects: 74 | newname = obj + '\\' + name 75 | return newname, objects.get(newname) 76 | 77 | def resolve_xref(self, env, fromdocname, builder, typ, target, node, 78 | contnode): 79 | objectname = node.get('php:object') 80 | searchorder = node.hasattr('refspecific') and 1 or 0 81 | name, obj = self.find_obj(env, objectname, target, typ, searchorder) 82 | if not obj: 83 | return None 84 | return make_refnode(builder, fromdocname, obj[0], name, contnode, name) 85 | 86 | def get_objects(self): 87 | for refname, (docname, type) in self.data['objects'].iteritems(): 88 | yield refname, refname, type, docname, refname, 1 89 | -------------------------------------------------------------------------------- /docs/_exts/sensio/sphinx/phpcode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | :copyright: (c) 2010-2012 Fabien Potencier 4 | :license: MIT, see LICENSE for more details. 5 | """ 6 | 7 | from docutils import nodes, utils 8 | 9 | from sphinx.util.nodes import split_explicit_title 10 | from string import lower 11 | 12 | def php_namespace_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 13 | text = utils.unescape(text) 14 | env = inliner.document.settings.env 15 | base_url = env.app.config.api_url 16 | has_explicit_title, title, namespace = split_explicit_title(text) 17 | 18 | try: 19 | full_url = base_url % namespace.replace('\\', '/') + '.html' 20 | except (TypeError, ValueError): 21 | env.warn(env.docname, 'unable to expand %s api_url with base ' 22 | 'URL %r, please make sure the base contains \'%%s\' ' 23 | 'exactly once' % (typ, base_url)) 24 | full_url = base_url + utils.escape(full_class) 25 | if not has_explicit_title: 26 | name = namespace.lstrip('\\') 27 | ns = name.rfind('\\') 28 | if ns != -1: 29 | name = name[ns+1:] 30 | title = name 31 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=namespace)] 32 | pnode = nodes.literal('', '', *list) 33 | return [pnode], [] 34 | 35 | def php_class_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 36 | text = utils.unescape(text) 37 | env = inliner.document.settings.env 38 | base_url = env.app.config.api_url 39 | has_explicit_title, title, full_class = split_explicit_title(text) 40 | 41 | try: 42 | full_url = base_url % full_class.replace('\\', '/') + '.html' 43 | except (TypeError, ValueError): 44 | env.warn(env.docname, 'unable to expand %s api_url with base ' 45 | 'URL %r, please make sure the base contains \'%%s\' ' 46 | 'exactly once' % (typ, base_url)) 47 | full_url = base_url + utils.escape(full_class) 48 | if not has_explicit_title: 49 | class_name = full_class.lstrip('\\') 50 | ns = class_name.rfind('\\') 51 | if ns != -1: 52 | class_name = class_name[ns+1:] 53 | title = class_name 54 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=full_class)] 55 | pnode = nodes.literal('', '', *list) 56 | return [pnode], [] 57 | 58 | def php_method_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 59 | text = utils.unescape(text) 60 | env = inliner.document.settings.env 61 | base_url = env.app.config.api_url 62 | has_explicit_title, title, class_and_method = split_explicit_title(text) 63 | 64 | ns = class_and_method.rfind('::') 65 | full_class = class_and_method[:ns] 66 | method = class_and_method[ns+2:] 67 | 68 | try: 69 | full_url = base_url % full_class.replace('\\', '/') + '.html' + '#method_' + method 70 | except (TypeError, ValueError): 71 | env.warn(env.docname, 'unable to expand %s api_url with base ' 72 | 'URL %r, please make sure the base contains \'%%s\' ' 73 | 'exactly once' % (typ, base_url)) 74 | full_url = base_url + utils.escape(full_class) 75 | if not has_explicit_title: 76 | title = method + '()' 77 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=full_class + '::' + method + '()')] 78 | pnode = nodes.literal('', '', *list) 79 | return [pnode], [] 80 | 81 | def php_phpclass_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 82 | text = utils.unescape(text) 83 | has_explicit_title, title, full_class = split_explicit_title(text) 84 | 85 | full_url = 'http://php.net/manual/en/class.%s.php' % lower(full_class) 86 | 87 | if not has_explicit_title: 88 | title = full_class 89 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=full_class)] 90 | pnode = nodes.literal('', '', *list) 91 | return [pnode], [] 92 | 93 | def php_phpmethod_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 94 | text = utils.unescape(text) 95 | has_explicit_title, title, class_and_method = split_explicit_title(text) 96 | 97 | ns = class_and_method.rfind('::') 98 | full_class = class_and_method[:ns] 99 | method = class_and_method[ns+2:] 100 | 101 | full_url = 'http://php.net/manual/en/%s.%s.php' % (lower(full_class), lower(method)) 102 | 103 | if not has_explicit_title: 104 | title = full_class + '::' + method + '()' 105 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=full_class)] 106 | pnode = nodes.literal('', '', *list) 107 | return [pnode], [] 108 | 109 | def php_phpfunction_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 110 | text = utils.unescape(text) 111 | has_explicit_title, title, full_function = split_explicit_title(text) 112 | 113 | full_url = 'http://php.net/manual/en/function.%s.php' % lower(full_function.replace('_', '-')) 114 | 115 | if not has_explicit_title: 116 | title = full_function 117 | list = [nodes.reference(title, title, internal=False, refuri=full_url, reftitle=full_function)] 118 | pnode = nodes.literal('', '', *list) 119 | return [pnode], [] 120 | 121 | def setup(app): 122 | app.add_config_value('api_url', {}, 'env') 123 | app.add_role('namespace', php_namespace_role) 124 | app.add_role('class', php_class_role) 125 | app.add_role('method', php_method_role) 126 | app.add_role('phpclass', php_phpclass_role) 127 | app.add_role('phpmethod', php_phpmethod_role) 128 | app.add_role('phpfunction', php_phpfunction_role) 129 | -------------------------------------------------------------------------------- /docs/_exts/sensio/sphinx/refinclude.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | :copyright: (c) 2010-2012 Fabien Potencier 4 | :license: MIT, see LICENSE for more details. 5 | """ 6 | 7 | from docutils.parsers.rst import Directive, directives 8 | from docutils import nodes 9 | 10 | class refinclude(nodes.General, nodes.Element): 11 | pass 12 | 13 | class RefInclude(Directive): 14 | has_content = False 15 | required_arguments = 1 16 | optional_arguments = 0 17 | final_argument_whitespace = False 18 | option_spec = {} 19 | 20 | def run(self): 21 | document = self.state.document 22 | 23 | if not document.settings.file_insertion_enabled: 24 | return [document.reporter.warning('File insertion disabled', 25 | line=self.lineno)] 26 | 27 | env = self.state.document.settings.env 28 | target = self.arguments[0] 29 | 30 | node = refinclude() 31 | node['target'] = target 32 | 33 | return [node] 34 | 35 | def process_refinclude_nodes(app, doctree, docname): 36 | env = app.env 37 | for node in doctree.traverse(refinclude): 38 | docname, labelid, sectname = env.domaindata['std']['labels'].get(node['target'], 39 | ('','','')) 40 | 41 | if not docname: 42 | return [document.reporter.error('Unknown target name: "%s"' % node['target'], 43 | line=self.lineno)] 44 | 45 | resultnode = None 46 | dt = env.get_doctree(docname) 47 | for n in dt.traverse(nodes.section): 48 | if labelid in n['ids']: 49 | node.replace_self([n]) 50 | break 51 | 52 | def setup(app): 53 | app.add_node(refinclude) 54 | app.add_directive('include-ref', RefInclude) 55 | app.connect('doctree-resolved', process_refinclude_nodes) 56 | -------------------------------------------------------------------------------- /docs/_static/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/_static/.placeholder -------------------------------------------------------------------------------- /docs/_templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/_templates/.placeholder -------------------------------------------------------------------------------- /docs/configure.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Configuration 3 | ============= 4 | 5 | Phormium uses a configuration array to configure the databases to which to 6 | connect. JSON and YAML files are also supported. To configure Phormium, pass the 7 | configuration array, or a path to the configuration file to 8 | ``Phormium\Orm::configure()``. 9 | 10 | The configuration array comprises of the following options: 11 | 12 | `databases` 13 | Configuration for one or more databases to which you wish to connect, 14 | indexed by a database name which is used in the model to determine in which 15 | database the table is located. 16 | 17 | Databases 18 | --------- 19 | 20 | Each entry in ``databases`` has the following configuration options: 21 | 22 | `dsn` 23 | The Data Source Name, or DSN, contains the information required to connect 24 | to the database. See `PDO documentation`_ for more information. 25 | 26 | `username` 27 | The username used to connect to the database. 28 | 29 | `password` 30 | The username used to connect to the database. 31 | 32 | `attributes` 33 | Associative array of PDO attributes with corresponding values to be set on 34 | the PDO connection after it has been created. 35 | 36 | When using a configuration array PDO constants can be used directly 37 | (e.g. ``PDO::ATTR_CASE``), whereas when using a config file, the constant 38 | can be given as a string (e.g. ``"PDO::ATTR_CASE"``) instead. 39 | 40 | For available attributes see the `PDO attributes`_ documentation. 41 | 42 | .. _PDO documentation: http://www.php.net/manual/en/pdo.construct.php 43 | .. _PDO attributes: http://php.net/manual/en/pdo.setattribute.php 44 | 45 | Examples 46 | -------- 47 | 48 | PHP example 49 | ~~~~~~~~~~~ 50 | 51 | .. code-block:: php 52 | 53 | Phormium\Orm::configure([ 54 | "databases" => [ 55 | "db1" => [ 56 | "dsn" => "mysql:host=localhost;dbname=db1", 57 | "username" => "myuser", 58 | "password" => "mypass", 59 | "attributes" => [ 60 | PDO::ATTR_CASE => PDO::CASE_LOWER, 61 | PDO::ATTR_STRINGIFY_FETCHES => true 62 | ] 63 | ], 64 | "db2" => [ 65 | "dsn" => "sqlite:/path/to/db2.sqlite" 66 | ] 67 | ] 68 | ]); 69 | 70 | .. note:: Short array syntax `[ ... ]` requires PHP 5.4+. 71 | 72 | JSON example 73 | ~~~~~~~~~~~~ 74 | 75 | This is the equivalent configuration in JSON. 76 | 77 | .. code-block:: javascript 78 | 79 | { 80 | "databases": { 81 | "db1": { 82 | "dsn": "mysql:host=localhost;dbname=db1", 83 | "username": "myuser", 84 | "password": "mypass", 85 | "attributes": { 86 | "PDO::ATTR_CASE": "PDO::CASE_LOWER", 87 | "PDO::ATTR_STRINGIFY_FETCHES": true 88 | } 89 | }, 90 | "db2": { 91 | "dsn": "sqlite:\/path\/to\/db2.sqlite" 92 | } 93 | } 94 | } 95 | 96 | .. code-block:: php 97 | 98 | Phormium\Orm::configure('/path/to/config.json'); 99 | 100 | YAML example 101 | ~~~~~~~~~~~~ 102 | 103 | This is the equivalent configuration in YAML. 104 | 105 | .. code-block:: yaml 106 | 107 | databases: 108 | db1: 109 | dsn: 'mysql:host=localhost;dbname=db1' 110 | username: myuser 111 | password: mypass 112 | attributes: 113 | 'PDO::ATTR_CASE': 'PDO::CASE_LOWER' 114 | 'PDO::ATTR_STRINGIFY_FETCHES': true 115 | db2: 116 | dsn: 'sqlite:/path/to/db2.sqlite' 117 | 118 | .. code-block:: php 119 | 120 | Phormium\Orm::configure('/path/to/config.yaml'); 121 | 122 | -------------------------------------------------------------------------------- /docs/images/relation-composite.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Post 25 | date (PK) 26 | no (PK) 27 | content 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Tag 48 | id (PK) 49 | post_date (FK) 50 | post_no (FK) 51 | value 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/images/relation-composite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/images/relation-composite.jpg -------------------------------------------------------------------------------- /docs/images/relation.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Person 25 | id (PK) 26 | name 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Contact 47 | id (PK) 48 | person_id (FK) 49 | value 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/images/relation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/docs/images/relation.jpg -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Phormium documentation 2 | ====================== 3 | 4 | Phormium is a minimalist ORM for PHP. 5 | 6 | It's tested on informix, mysql, postgresql and sqlite. 7 | 8 | Could work with other relational databases which have a PDO driver, or may 9 | require some changes. 10 | 11 | .. caution:: This is a work in progress. Test before using! Report any bugs 12 | on `Github `_. 13 | 14 | Contents 15 | -------- 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | install 21 | setup 22 | configure 23 | usage 24 | transactions 25 | events 26 | relations 27 | 28 | License 29 | ------- 30 | 31 | Copyright (c) 2012 Ivan Habunek 32 | 33 | Permission is hereby granted, free of charge, to any person obtaining a copy of 34 | this software and associated documentation files (the "Software"), to deal in 35 | the Software without restriction, including without limitation the rights to 36 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 37 | of the Software, and to permit persons to whom the Software is furnished to do 38 | so, subject to the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be included in 41 | copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 44 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 45 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 46 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 47 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 48 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 49 | SOFTWARE. -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Prerequisites 6 | ------------- 7 | 8 | Phormium requires PHP 5.6 or greater with the PDO_ extension loaded, as well as 9 | any PDO drivers for databases to wich you wish to connect. 10 | 11 | .. _PDO: http://php.net/manual/en/book.pdo.php 12 | 13 | Via Composer 14 | ------------ 15 | 16 | The most flexible installation method is using Composer. 17 | 18 | Create a `composer.json` file in the root of your project: 19 | 20 | .. code-block:: javascript 21 | 22 | { 23 | "require": { 24 | "phormium/phormium": "0.*" 25 | } 26 | } 27 | 28 | Install composer: 29 | 30 | .. code-block:: bash 31 | 32 | curl -s http://getcomposer.org/installer | php 33 | 34 | Run Composer to install Phormium: 35 | 36 | .. code-block:: bash 37 | 38 | php composer.phar install 39 | 40 | To upgrade Phormium to the latest version, run: 41 | 42 | .. code-block:: bash 43 | 44 | php composer.phar update 45 | 46 | Once installed, include `vendor/autoload.php` in your script to autoload 47 | Phormium. 48 | 49 | .. code-block:: bash 50 | 51 | require 'vendor/autoload.php'; 52 | 53 | From GitHub 54 | ----------- 55 | 56 | The alternative is to checkout the code directly from GitHub: 57 | 58 | .. code-block:: bash 59 | 60 | git clone https://github.com/ihabunek/phormium.git 61 | 62 | In your code, include and register the Phormium autoloader: 63 | 64 | .. code-block:: bash 65 | 66 | require 'phormium/Phormium/Autoloader.php'; 67 | \Phormium\Autoloader::register(); 68 | 69 | Once you have installed Phormium, the next step is to :doc:`set it up `. 70 | -------------------------------------------------------------------------------- /docs/relations.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Relations 3 | ========= 4 | 5 | Phormium allows you to define relations between models for tables which are 6 | linked via a foreign key. 7 | 8 | Consider a Person and Contact tables like these: 9 | 10 | .. image:: ./images/relation.jpg 11 | 12 | The Contact table has a foreign key which references the Person table via 13 | the ``person_id`` field. This makes Person the parent table, and Contact the 14 | child table. Each Person record can have zero or more Contact records. 15 | 16 | To keep things simple, relations are not defined in the model meta-data, but 17 | by invoking the following methods on the Model: 18 | 19 | * ``hasChildren()`` method can be invoked on the parent model (Person), and 20 | will return a QuerySet for the child model (Contact) which is filtered to 21 | include all the child records linked to the parent model on which the method 22 | is executed. This QuerySet can contain zero or more records. 23 | 24 | * ``hasParent()`` method can be invoked on the child model (Contact), and will 25 | return a QuerySet for the parent model (Person) which is filtered to include 26 | it's parent Person record. 27 | 28 | Example 29 | ------- 30 | 31 | Models for these tables might look like this: 32 | 33 | .. code-block:: php 34 | 35 | class Person extends Phormium\Model 36 | { 37 | protected static $_meta = array( 38 | 'database' => 'exampledb', 39 | 'table' => 'person', 40 | 'pk' => 'id' 41 | ); 42 | 43 | public $id; 44 | 45 | public $name; 46 | 47 | public function contacts() 48 | { 49 | return $this->hasChildren("Contact"); 50 | } 51 | } 52 | 53 | .. code-block:: php 54 | 55 | class Contact extends Phormium\Model 56 | { 57 | protected static $_meta = array( 58 | 'database' => 'exampledb', 59 | 'table' => 'contact', 60 | 'pk' => 'id' 61 | ); 62 | 63 | public $id; 64 | 65 | public $person_id; 66 | 67 | public $value; 68 | 69 | public function person() 70 | { 71 | return $this->hasParent("Person"); 72 | } 73 | } 74 | 75 | Note that these functions return a filtered QuerySet, so you need to call 76 | one of the fetching methods to fetch the data. 77 | 78 | .. code-block:: php 79 | 80 | // Fetching person's contacts 81 | $person = Person::get(1); 82 | $contacts = $person->contacts()->fetch(); 83 | 84 | // Fetching contact's person 85 | $contact = Contact::get(5); 86 | $person = $contact->person()->single(); 87 | 88 | 89 | Returning a QuerySet allows you to further filter the result. For example, to 90 | return person's contact whose value is not null: 91 | 92 | .. code-block:: php 93 | 94 | $person = Person::get(1); 95 | 96 | $contacts = $person->contacts() 97 | ->filter('value', 'NOT NULL') 98 | ->fetch(); 99 | 100 | Overriding defaults 101 | ------------------- 102 | 103 | Phormium does it's best to guess the names of the foreign key column(s) in both 104 | tables. The guesswork, however depends on: 105 | 106 | * Naming classes in CamelCase (e.g. ``FooBar``) 107 | * Naming tables in lowercase using underscores (e.g. ``foo_bar``) 108 | * Naming foreign keys which reference the ``foo_bar`` table ``foo_bar_$id``, 109 | where ``$id`` is the name of the primary key column in ``some_table``. 110 | 111 | The following code: 112 | 113 | .. code-block:: php 114 | 115 | $this->hasChildren("Contact"); 116 | 117 | is shorthand for: 118 | 119 | .. code-block:: php 120 | 121 | $this->hasChildren("Contact", "person_id", "id"); 122 | 123 | where ``person_id`` is the name of the foreign key column in the child table 124 | (Contact), and ``id`` is the name of the referenced primary key column in the 125 | parent table (Person). 126 | 127 | If your keys are named differently, you can override these settings. For 128 | example: 129 | 130 | .. code-block:: php 131 | 132 | $this->hasChildren("Contact", "owner_id"); 133 | 134 | Composite keys 135 | -------------- 136 | 137 | Relations also work for tables with composite primary/foreign keys. 138 | 139 | For example, consider these tables: 140 | 141 | .. image:: ./images/relation-composite.jpg 142 | 143 | Models for these tables can be implemented as: 144 | 145 | .. code-block:: php 146 | 147 | class Post extends Phormium\Model 148 | { 149 | protected static $_meta = array( 150 | 'database' => 'exampledb', 151 | 'table' => 'post', 152 | 'pk' => ['date', 'no'] 153 | ); 154 | 155 | public $date; 156 | 157 | public $no; 158 | 159 | public $content; 160 | 161 | public function tags() 162 | { 163 | return $this->hasChildren("Tag"); 164 | } 165 | } 166 | 167 | .. code-block:: php 168 | 169 | class Tag extends Phormium\Model 170 | { 171 | protected static $_meta = array( 172 | 'database' => 'exampledb', 173 | 'table' => 'tag', 174 | 'pk' => 'id' 175 | ); 176 | 177 | public $id; 178 | 179 | public $post_date; 180 | 181 | public $post_no; 182 | 183 | public $value; 184 | 185 | public function post() 186 | { 187 | return $this->hasParent("Post"); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=1.2.2 2 | sphinx_rtd_theme -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Setting up 3 | ========== 4 | 5 | Unlike some ORMs, Phormium does not automatically generate the database model or 6 | the PHP classes onto which the model is mapped. This has to be done manually. 7 | 8 | Configure database connections 9 | ------------------------------ 10 | 11 | Create a JSON configuration file which contains database definitions you wish to 12 | use. Each database must have a DSN string, and optional username and password if 13 | required. 14 | 15 | .. code-block:: javascript 16 | 17 | { 18 | "databases": { 19 | "testdb": { 20 | "dsn": "mysql:host=localhost;dbname=testdb", 21 | "username": "myuser", 22 | "password": "mypass" 23 | } 24 | } 25 | } 26 | 27 | 28 | For details on database specific DSNs consult the `PHP documentation 29 | `_. 30 | 31 | A more detailed config file reference can be found in the :doc:`configuration 32 | chapter `. 33 | 34 | Create a database model 35 | ----------------------- 36 | 37 | You need a database table which will be mapped. For example, the following SQL 38 | will create a MySQL table called `person`: 39 | 40 | .. code-block:: sql 41 | 42 | CREATE TABLE person ( 43 | id INTEGER PRIMARY KEY AUTOINCREMENT, 44 | name VARCHAR(100), 45 | birthday DATE, 46 | salary DECIMAL 47 | ); 48 | 49 | The table does not have to have a primary key, but if it doesn't Phormium will 50 | not perform update or delete queries. 51 | 52 | Create a Model class 53 | -------------------- 54 | 55 | To map the `person` table onto a PHP class, a corresponding Model class is 56 | defined. Although this class can be called anything, it's sensible to name it 57 | the same as the table being mapped. 58 | 59 | .. code-block:: php 60 | 61 | class Person extends Phormium\Model 62 | { 63 | // Mapping meta-data 64 | protected static $_meta = array( 65 | 'database' => 'testdb', 66 | 'table' => 'person', 67 | 'pk' => 'id' 68 | ); 69 | 70 | // Table columns 71 | public $id; 72 | public $name; 73 | public $birthday; 74 | public $salary; 75 | } 76 | 77 | Public properties of the `Person` class match the column names of the `person` 78 | database table. 79 | 80 | Additionaly, a protected static `$_meta` property is required which holds an 81 | array with the following values: 82 | 83 | `database` 84 | Name of the database, as defined in the configuration. 85 | `table` 86 | Name of the database table to which the model maps. 87 | `pk` 88 | Name of the primary key column (or an array of names for composite primary 89 | keys). If not defined, will default to "id", if that column exists. 90 | 91 | Try it out 92 | ---------- 93 | 94 | Create a few test rows in the database table and run the following code to fetch 95 | them: 96 | 97 | .. code-block:: php 98 | 99 | require 'vendor/autoload.php'; 100 | require 'Person.php'; 101 | 102 | Phormium\Orm::configure('config.json'); 103 | 104 | $persons = Person::objects()->fetch(); 105 | 106 | Learn more about usage in the :doc:`next chapter `. 107 | -------------------------------------------------------------------------------- /docs/transactions.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Transactions 3 | ============ 4 | 5 | Phormium has two ways of using transactions. 6 | 7 | The transaction is global, meaning it will be started an all required database 8 | connections without the need to know which model is mapped to which database. 9 | 10 | Callback transactions 11 | --------------------- 12 | 13 | By passing a callable to `Orm::transaction()`, the code within the callable will 14 | be executed within a transaction. If an exception is thrown within the callback, 15 | the transaction will be rolled back. Otherwise it will be commited once the 16 | callback is executed. 17 | 18 | For example, if you wanted to increase the salary for several Persons, you might 19 | code it this way: 20 | 21 | .. code-block:: php 22 | 23 | $ids = array(10, 20, 30); 24 | $increment = 100; 25 | 26 | Orm::transaction(function() use ($ids, $increment) { 27 | foreach ($ids as $id) { 28 | $p = Person::get($id); 29 | $p->income += $increment; 30 | $p->save(); 31 | } 32 | }); 33 | 34 | If any of the person IDs from `$ids` does not exist, `Person::get($id)` will 35 | raise an exception which will roll back any earlier changes done within the 36 | callback. 37 | 38 | Manual transactions 39 | ------------------- 40 | 41 | It is also possible to control the transaction manaully, however this produces 42 | somewhat less readable code. 43 | 44 | Equivalent to the callback example would look like: 45 | 46 | .. code-block:: php 47 | 48 | $ids = array(10, 20, 30); 49 | $increment = 100; 50 | 51 | Orm::begin(); 52 | 53 | try { 54 | foreach ($ids as $id) { 55 | $p = Person::get($id); 56 | $p->income += $increment; 57 | $p->save(); 58 | } 59 | } catch (\Exception $ex) { 60 | Orm::rollback(); 61 | throw new \Exception("Transaction failed. Rolled back.", 0, $ex); 62 | } 63 | 64 | Orm::commit(); 65 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Phormium Example using SQLite 2 | ============================= 3 | The prerequisite for these examples is the 4 | [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) extension so 5 | Phormium can to connect to the test database. 6 | 7 | You may also want to have [sqlite](http://www.sqlite.org/) in order to 8 | (re)create the test database. 9 | 10 | The test database `example.sq3` is provided, but if you wish to recreate it, 11 | you can run the following in the example dir: 12 | 13 | sqlite3 example.sq3 < setup.sql 14 | 15 | Database config file (config.json) 16 | ---------------------------------- 17 | The Phormium configuration file defines where the database is located. 18 | 19 | Model classes 20 | ------------- 21 | 22 | This example uses several Model classes, which are located in the `models` 23 | directory. Model classes extend `Phormium\Model` and map onto a database table. 24 | 25 | Public properties of each Model class match the column names of the 26 | corresponding database table. 27 | 28 | Additionaly, they contain a protected static `$_meta` array which contains 29 | the following values: 30 | - database - name of the database, as defined in `config.json` 31 | - table - name of the database table 32 | - pk - the primary key column(s) 33 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "exampledb": { 4 | "dsn": "sqlite:example.sq3", 5 | "username": "", 6 | "password": "" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /example/events.php: -------------------------------------------------------------------------------- 1 | on('query.started', array($this, 'started')); 27 | Orm::emitter()->on('query.completed', array($this, 'completed')); 28 | } 29 | 30 | /** Called when a query has started. */ 31 | public function started($query, $arguments) 32 | { 33 | $this->active = array( 34 | 'query' => $query, 35 | 'arguments' => $arguments, 36 | 'start' => microtime(true) 37 | ); 38 | } 39 | 40 | /** Called when a query has completed. */ 41 | public function completed($query) 42 | { 43 | $active = $this->active; 44 | 45 | $active['end'] = microtime(true); 46 | $active['duration'] = $active['end'] - $active['start']; 47 | 48 | $this->stats[] = $active; 49 | $this->active = null; 50 | } 51 | 52 | /** Returns the collected statistics. */ 53 | public function getStats() 54 | { 55 | return $this->stats; 56 | } 57 | } 58 | 59 | $stats = new Stats(); 60 | $stats->register(); 61 | 62 | // Execute some queries 63 | Person::find(10); 64 | Person::objects()->fetch(); 65 | 66 | // Print collected statistics 67 | print_r($stats->getStats()); 68 | -------------------------------------------------------------------------------- /example/example.sq3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/example/example.sq3 -------------------------------------------------------------------------------- /example/models-read.php: -------------------------------------------------------------------------------- 1 | "Freddy Mercury" 26 | ]); 27 | $person->save(); 28 | $personID = $person->id; 29 | 30 | $postDate = date('Y-m-d'); 31 | $postNo = 1; 32 | $post = Post::fromArray([ 33 | 'date' => $postDate, 34 | 'no' => $postNo, 35 | 'title' => "My only post" 36 | ]); 37 | $post->save(); 38 | 39 | /** 40 | * To fetch a single record by it's primary key use: 41 | * - Model::get() - throws an exception if record does not exist 42 | * - Model::find() - returns NULL if record does not exist 43 | */ 44 | 45 | $person = Person::get($personID); 46 | $person = Person::find($personID); 47 | 48 | /** 49 | * Also works for composite primary keys. 50 | */ 51 | 52 | $post = Post::get($postDate, $postNo); 53 | $post = Post::find($postDate, $postNo); 54 | 55 | /** 56 | * You can pass the composite primary key as an array. 57 | */ 58 | 59 | $postID = array($postDate, $postNo); 60 | $post = Post::get($postID); 61 | $post = Post::find($postID); 62 | 63 | echo SEPARATOR . "This is person #10:\n"; 64 | print_r($person); 65 | 66 | echo SEPARATOR . "This is Post $postDate #$postNo:\n"; 67 | print_r($post); 68 | 69 | /** 70 | * To check if a model exists by primary key, without fetching it, use 71 | * Model::exists(). This returns a boolean. 72 | */ 73 | 74 | $ex1 = Person::exists($personID); 75 | $ex2 = Person::exists(999); 76 | 77 | $ex3 = Post::exists($postDate, $postNo); 78 | $ex4 = Post::exists($postDate, 999); 79 | 80 | echo SEPARATOR; 81 | 82 | echo "Person #$personID exists: "; 83 | var_dump($ex1); 84 | 85 | echo "Person #999 exists: "; 86 | var_dump($ex2); 87 | 88 | echo "Post $postDate $postNo exists: "; 89 | var_dump($ex3); 90 | 91 | echo "Post $postDate 999 exists: "; 92 | var_dump($ex4); 93 | -------------------------------------------------------------------------------- /example/models-write.php: -------------------------------------------------------------------------------- 1 | name = "Frank Zappa"; 27 | $person->birthday = "1940-12-21"; 28 | $person->salary = 1000; 29 | $person->insert(); 30 | 31 | echo SEPARATOR . "New person inserted:\n"; 32 | print_r($person); 33 | 34 | /** 35 | * To create a Model from data contained in an array, use the merge() method to 36 | * overwrite any data in the model with the data from the array. 37 | */ 38 | 39 | $data = array( 40 | 'name' => 'Captain Beefheart', 41 | 'birthday' => '1941-01-15', 42 | 'salary' => 1200 43 | ); 44 | 45 | $person = new Person(); 46 | $person->merge($data); 47 | $person->insert(); 48 | 49 | echo SEPARATOR . "New person inserted:\n"; 50 | print_r($person); 51 | 52 | /** 53 | * To change an existing record, fetch it from the database, perform the 54 | * required changes and call update(). 55 | */ 56 | 57 | $personID = $person->id; 58 | 59 | echo SEPARATOR . "Person #$personID before changes:\n"; 60 | print_r(Person::get($personID)); 61 | 62 | // Get, change, update 63 | $person = Person::get($personID); 64 | $person->salary += 500; 65 | $person->update(); 66 | 67 | echo SEPARATOR . "Person #$personID after changes:\n"; 68 | print_r(Person::get($personID)); 69 | 70 | /** 71 | * The magic save() method will automatically update() the record if it exists 72 | * and insert() it if it doesn't. It can be used instead of update() and 73 | * insert(), but it can be sub-optimal since it queries the database to check 74 | * if a record exists. 75 | */ 76 | 77 | // Both of these work: 78 | $person = new Person(); 79 | $person->name = "Frank Zappa"; 80 | $person->birthday = "1940-12-21"; 81 | $person->salary = 1000; 82 | $person->save(); 83 | 84 | $person = Person::get($personID); 85 | $person->salary += 500; 86 | $person->save(); 87 | -------------------------------------------------------------------------------- /example/models/Contact.php: -------------------------------------------------------------------------------- 1 | 'exampledb', 7 | 'table' => 'contact', 8 | 'pk' => 'id' 9 | ); 10 | 11 | public $id; 12 | public $person_id; 13 | public $value; 14 | 15 | public function person() 16 | { 17 | return $this->hasParent("Person"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/models/Person.php: -------------------------------------------------------------------------------- 1 | 'exampledb', 7 | 'table' => 'person', 8 | 'pk' => 'id' 9 | ); 10 | 11 | public $id; 12 | public $name; 13 | public $birthday; 14 | public $salary; 15 | 16 | public function contacts() 17 | { 18 | return $this->hasChildren("Contact"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/models/Post.php: -------------------------------------------------------------------------------- 1 | 'exampledb', 12 | 'table' => 'post', 13 | 'pk' => ['date', 'no'] 14 | ); 15 | 16 | public $date; 17 | 18 | public $no; 19 | 20 | public $title; 21 | 22 | public $contents; 23 | 24 | public function tags() 25 | { 26 | return $this->hasChildren('Tag'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/models/Tag.php: -------------------------------------------------------------------------------- 1 | 'exampledb', 7 | 'table' => 'tag', 8 | 'pk' => 'id' 9 | ); 10 | 11 | public $id; 12 | 13 | public $post_date; 14 | 15 | public $post_no; 16 | 17 | public $value; 18 | 19 | public function post() 20 | { 21 | return $this->hasParent("Post"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/querysets-read.php: -------------------------------------------------------------------------------- 1 | fetch(); 31 | 32 | echo SEPARATOR . "The person table has " . count($persons) . " records.\n"; 33 | 34 | /** 35 | * To limit the output, the results can be filtered. 36 | */ 37 | 38 | $persons = Person::objects() 39 | ->filter('salary', '>', 5000) 40 | ->fetch(); 41 | 42 | echo SEPARATOR . "The person table has " . count($persons) . " records with salary over 5000.\n"; 43 | 44 | /** 45 | * Note that filter() will return a new instance of QuerySet with the given 46 | * filter added to it, this allows chaining. 47 | */ 48 | 49 | $persons = Person::objects() 50 | ->filter('salary', '>', 5000) 51 | ->filter('name', 'like', 'M%') 52 | ->fetch(); 53 | 54 | echo SEPARATOR . "The person table has " . count($persons) . " records whose name starts with M and with salary over 5000.\n"; 55 | -------------------------------------------------------------------------------- /example/querysets-write.php: -------------------------------------------------------------------------------- 1 | filter('salary', '>', 5000) 26 | ->update(array( 27 | 'salary' => 6000 28 | )); 29 | 30 | echo SEPARATOR . "Updated $count rich people."; 31 | 32 | /** 33 | * Alternatively, you can delete them (commented out because it's destructive). 34 | */ 35 | 36 | // Person::objects() 37 | // ->filter('salary', '>', 5000) 38 | // ->delete(); 39 | -------------------------------------------------------------------------------- /example/relations.php: -------------------------------------------------------------------------------- 1 | 1, "name" => "Ivan"])->save(); 25 | Contact::fromArray(["id" => 1, "person_id" => 1, "value" => "foo"])->save(); 26 | Contact::fromArray(["id" => 2, "person_id" => 1, "value" => "bar"])->save(); 27 | Contact::fromArray(["id" => 3, "person_id" => 1, "value" => "baz"])->save(); 28 | 29 | // Fetch the person, then get her contacts 30 | $person = Person::get(1); 31 | $contacts = $person->contacts()->fetch(); 32 | print_r($contacts); 33 | 34 | // Fetch the contact, then get the person it belongs to 35 | $contact = Contact::get(1); 36 | $person = $contact->person()->single(); 37 | print_r($person); 38 | 39 | // Create a post and three tags 40 | Post::fromArray(["date" => $date, "no" => 1, "title" => "Post #1"])->save(); 41 | Tag::fromArray(["id" => 1, "post_date" => $date, "post_no" => 1, "value" => "Tag #1"])->save(); 42 | Tag::fromArray(["id" => 2, "post_date" => $date, "post_no" => 1, "value" => "Tag #2"])->save(); 43 | Tag::fromArray(["id" => 3, "post_date" => $date, "post_no" => 1, "value" => "Tag #3"])->save(); 44 | 45 | // Fetch a post, then fetch it's tags 46 | $post = Post::get($date, 1); 47 | $tags = $post->tags()->fetch(); 48 | print_r($tags); 49 | 50 | // Fetch a tag, then fetch the post it belongs to 51 | $tag = Tag::get(2); 52 | $post = $tag->post()->single(); 53 | print_r($post); 54 | -------------------------------------------------------------------------------- /example/setup.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS person; 2 | CREATE TABLE person( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | name VARCHAR(100), 5 | birthday DATE, 6 | salary DECIMAL(20,2) 7 | ); 8 | 9 | DROP TABLE IF EXISTS contact; 10 | CREATE TABLE contact( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT, 12 | person_id INTEGER, 13 | value VARCHAR(255), 14 | FOREIGN KEY (person_id) REFERENCES person(id) 15 | ); 16 | 17 | DROP TABLE IF EXISTS post; 18 | CREATE TABLE post( 19 | date DATE, 20 | no INTEGER, 21 | title VARCHAR(255), 22 | contents VARCHAR(1024), 23 | PRIMARY KEY (date, no) 24 | ); 25 | 26 | DROP TABLE IF EXISTS tag; 27 | CREATE TABLE tag( 28 | id INTEGER PRIMARY KEY AUTOINCREMENT, 29 | post_date DATE, 30 | post_no INTEGER, 31 | value VARCHAR(1024), 32 | FOREIGN KEY (post_date, post_no) REFERENCES post(date, no) 33 | ); 34 | 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/unit 5 | 6 | 7 | tests/integration 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Phormium/Autoloader.php: -------------------------------------------------------------------------------- 1 | DIRECTORY_SEPARATOR, 33 | '_' => DIRECTORY_SEPARATOR 34 | ]; 35 | 36 | $subpath = substr($class, strlen($namespace)); 37 | $subpath = strtr($subpath, $replacements); 38 | $path = __DIR__ . DIRECTORY_SEPARATOR . $subpath . ".php"; 39 | 40 | if (file_exists($path)) { 41 | include $path; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Phormium/Config/ArrayLoader.php: -------------------------------------------------------------------------------- 1 | root('phormium'); 22 | 23 | $rootNode 24 | ->children() 25 | ->arrayNode('databases') 26 | ->prototype('array') 27 | ->children() 28 | ->scalarNode('dsn') 29 | ->isRequired() 30 | ->cannotBeEmpty() 31 | ->end() 32 | ->scalarNode('username') 33 | ->defaultNull() 34 | ->end() 35 | ->scalarNode('password') 36 | ->defaultNull() 37 | ->end() 38 | ->arrayNode('attributes') 39 | ->useAttributeAsKey('name') 40 | ->prototype('scalar') 41 | ->isRequired() 42 | ->end() 43 | ->end() 44 | ->end() 45 | ->end() 46 | ->end() 47 | ->end() 48 | ; 49 | 50 | return $treeBuilder; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Phormium/Config/FileLoader.php: -------------------------------------------------------------------------------- 1 | loadFile($resource); 17 | 18 | try { 19 | return Json::parse($json, true); 20 | } catch (OrmException $ex) { 21 | throw new ConfigurationException("Failed parsing JSON configuration file.", 0, $ex); 22 | } 23 | } 24 | 25 | public function supports($resource, $type = null) 26 | { 27 | return is_string($resource) && 28 | pathinfo($resource, PATHINFO_EXTENSION) === 'json'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Phormium/Config/PostProcessor.php: -------------------------------------------------------------------------------- 1 | $dbConfig) { 15 | $config['databases'][$name] = $this->processDbConfig($name, $dbConfig); 16 | } 17 | 18 | return $config; 19 | } 20 | 21 | public function processDbConfig($name, $config) 22 | { 23 | // Ensure username and password keys exist 24 | if (!array_key_exists("username", $config)) { 25 | $config['username'] = null; 26 | } 27 | 28 | if (!array_key_exists("password", $config)) { 29 | $config['password'] = null; 30 | } 31 | 32 | // Add the driver name to database config, needed to tailor db queries 33 | $config['driver'] = $this->parseDriver($config['dsn']); 34 | 35 | // Convert string attributes to actual values 36 | $config['attributes'] = $this->processAttributes($name, $config['attributes']); 37 | 38 | return $config; 39 | } 40 | 41 | /** Parses the DSN and extracts the driver name. */ 42 | public function parseDriver($dsn) 43 | { 44 | $count = preg_match('/^([a-z]+):/', $dsn, $matches); 45 | 46 | if ($count !== 1) { 47 | throw new ConfigurationException("Invalid DSN: \"$dsn\". The DSN should start with ':'"); 48 | } 49 | 50 | return $matches[1]; 51 | } 52 | 53 | public function processAttributes($dbName, $attributes) 54 | { 55 | $processed = []; 56 | 57 | foreach ($attributes as $name => $value) { 58 | try { 59 | $procName = $this->processConstant($name, false); 60 | } catch (\Exception $ex) { 61 | throw new ConfigurationException("Invalid attribute \"$name\" specified in configuration for database \"$dbName\"."); 62 | } 63 | 64 | try { 65 | $procValue = $this->processConstant($value, true); 66 | } catch (\Exception $ex) { 67 | throw new ConfigurationException("Invalid value given for attribute \"$name\", in configuration for database \"$dbName\"."); 68 | } 69 | 70 | $processed[$procName] = $procValue; 71 | } 72 | 73 | return $processed; 74 | } 75 | 76 | public function processConstant($value, $allowScalar = false) 77 | { 78 | // If the value is an integer, assume it's a PDO::* constant value 79 | // and leave it as-is 80 | if (is_integer($value)) { 81 | return $value; 82 | } 83 | 84 | // If it's a string which starts with "PDO::", try to find the 85 | // corresponding PDO constant 86 | if (is_string($value) && substr($value, 0, 5) === 'PDO::' && defined($value)) { 87 | return constant($value); 88 | } 89 | 90 | if ($allowScalar && is_scalar($value)) { 91 | return $value; 92 | } 93 | 94 | throw new ConfigurationException("Invalid constant value"); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Phormium/Config/YamlLoader.php: -------------------------------------------------------------------------------- 1 | loadFile($resource); 15 | 16 | return Yaml::parse($data); 17 | } 18 | 19 | public function supports($resource, $type = null) 20 | { 21 | return is_string($resource) && 22 | in_array(pathinfo($resource, PATHINFO_EXTENSION), ['yaml', 'yml']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Phormium/Container.php: -------------------------------------------------------------------------------- 1 | getConfigTreeBuilder(); 42 | return $builder->buildTree(); 43 | }; 44 | 45 | $this['config.postprocessor'] = function () { 46 | return new PostProcessor(); 47 | }; 48 | 49 | $this['config'] = function () { 50 | // Load all given configurations 51 | $configs = []; 52 | foreach ($this['config.input'] as $raw) { 53 | $configs[] = $this['config.loader']->load($raw); 54 | } 55 | 56 | // Combine them and validate 57 | $config = $this['config.processor']->process( 58 | $this['config.tree'], 59 | $configs 60 | ); 61 | 62 | // Additional postprocessing to handle 63 | return $this['config.postprocessor']->processConfig($config); 64 | }; 65 | 66 | // Event emitter 67 | $this['emitter'] = function () { 68 | return new EventEmitter(); 69 | }; 70 | 71 | // Parser for model metadata 72 | $this['meta.builder'] = function () { 73 | return new MetaBuilder(); 74 | }; 75 | 76 | // Model metadata cache 77 | $this['meta.cache'] = function () { 78 | return new \ArrayObject(); 79 | }; 80 | 81 | // Database connection factory 82 | $this['database.factory'] = function () { 83 | return new Factory( 84 | $this['config']['databases'], 85 | $this['emitter'] 86 | ); 87 | }; 88 | 89 | // Database connection manager 90 | $this['database'] = function () { 91 | return new Database( 92 | $this['database.factory'], 93 | $this['emitter'] 94 | ); 95 | }; 96 | 97 | // Cache for query builders 98 | $this['query_builder.cache'] = function () { 99 | return new \ArrayObject(); 100 | }; 101 | 102 | // Query builder factory 103 | $this['query_builder.factory'] = function () { 104 | return new QueryBuilderFactory($this['query_builder.cache']); 105 | }; 106 | 107 | // Cache for Query objects 108 | $this['query.cache'] = function () { 109 | return new \ArrayObject(); 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Phormium/DB.php: -------------------------------------------------------------------------------- 1 | getConnection()"); 23 | return Orm::database()->getConnection($name); 24 | } 25 | 26 | public static function isConnected($name) 27 | { 28 | self::deprecationNotice(__METHOD__, "Orm::database()->isConnected()"); 29 | return Orm::database()->isConnected($name); 30 | } 31 | 32 | public static function setConnection($name, Connection $connection) 33 | { 34 | self::deprecationNotice(__METHOD__, "Orm::database()->setConnection()"); 35 | return Orm::database()->setConnection($name, $connection); 36 | } 37 | 38 | public static function disconnect($name) 39 | { 40 | self::deprecationNotice(__METHOD__, "Orm::database()->disconnect()"); 41 | return Orm::database()->disconnect($name); 42 | } 43 | 44 | public static function disconnectAll() 45 | { 46 | self::deprecationNotice(__METHOD__, "Orm::database()->disconnectAll()"); 47 | return Orm::database()->disconnectAll(); 48 | } 49 | 50 | public static function begin() 51 | { 52 | self::deprecationNotice(__METHOD__, "Orm::begin()"); 53 | return Orm::begin(); 54 | } 55 | 56 | public static function commit() 57 | { 58 | self::deprecationNotice(__METHOD__, "Orm::commit()"); 59 | return Orm::commit(); 60 | } 61 | 62 | public static function rollback() 63 | { 64 | self::deprecationNotice(__METHOD__, "Orm::rollback()"); 65 | return Orm::rollback(); 66 | } 67 | 68 | public static function transaction(callable $callback) 69 | { 70 | self::deprecationNotice(__METHOD__, "Orm::commit()"); 71 | return Orm::transaction($callback); 72 | } 73 | 74 | private static function deprecationNotice($method, $new) 75 | { 76 | $msg = "Method $method is deprecated and will be removed."; 77 | $msg .= " Please use $new instead."; 78 | trigger_error($msg, E_USER_WARNING); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Phormium/Database/Driver.php: -------------------------------------------------------------------------------- 1 | databases = $databases; 31 | $this->emitter = $emitter; 32 | } 33 | 34 | /** Creates a new connection. */ 35 | public function newConnection($name) 36 | { 37 | if (!isset($this->databases[$name])) { 38 | throw new DatabaseException("Database \"$name\" is not configured."); 39 | } 40 | 41 | // Extract settings 42 | $dsn = $this->databases[$name]['dsn']; 43 | $username = $this->databases[$name]['username']; 44 | $password = $this->databases[$name]['password']; 45 | $attributes = $this->databases[$name]['attributes']; 46 | 47 | // Create a PDO connection 48 | $pdo = new PDO($dsn, $username, $password); 49 | 50 | // Don't allow ATTR_ERRORMODE to be changed by the configuration, 51 | // because Phormium depends on errors throwing exceptions. 52 | if (isset($attributes[PDO::ATTR_ERRMODE]) 53 | && $attributes[PDO::ATTR_ERRMODE] !== PDO::ERRMODE_EXCEPTION) { 54 | // Warn the user 55 | $msg = "Phormium: Attribute PDO::ATTR_ERRMODE is set to something ". 56 | "other than PDO::ERRMODE_EXCEPTION for database \"$name\".". 57 | " This is not allowed because Phormium depends on this ". 58 | "setting. Skipping attribute definition."; 59 | 60 | trigger_error($msg, E_USER_WARNING); 61 | } 62 | 63 | $attributes[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; 64 | 65 | // Apply the attributes 66 | foreach ($attributes as $key => $value) { 67 | if (!$pdo->setAttribute($key, $value)) { 68 | throw new DatabaseException("Failed setting PDO attribute \"$key\" to \"$value\" on database \"$name\"."); 69 | } 70 | } 71 | 72 | return new Connection($pdo, $this->emitter); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Phormium/Event.php: -------------------------------------------------------------------------------- 1 | on(). Will be removed in 1.0.0. 30 | */ 31 | public static function on($event, $listener) 32 | { 33 | self::deprecationNotice(__METHOD__, "Orm::emitter()->on()"); 34 | return Orm::emitter()->on($event, $listener); 35 | } 36 | 37 | /** 38 | * @deprecated 0.9.0 Use Orm::emitter()->once(). Will be removed in 1.0.0. 39 | */ 40 | public static function once($event, $listener) 41 | { 42 | self::deprecationNotice(__METHOD__, "Orm::emitter()->once()"); 43 | return Orm::emitter()->once($event, $listener); 44 | } 45 | 46 | /** 47 | * @deprecated 0.9.0 Use Orm::emitter()->emit(). Will be removed in 1.0.0. 48 | */ 49 | public static function emit($event, array $arguments = []) 50 | { 51 | self::deprecationNotice(__METHOD__, "Orm::emitter()->emit()"); 52 | return Orm::emitter()->emit($event, $arguments); 53 | } 54 | 55 | /** 56 | * @deprecated 0.9.0 Use Orm::emitter()->listeners(). Will be removed in 1.0.0. 57 | */ 58 | public static function listeners($event) 59 | { 60 | self::deprecationNotice(__METHOD__, "Orm::emitter()->listeners()"); 61 | return Orm::emitter()->listeners($event); 62 | } 63 | 64 | /** 65 | * @deprecated 0.9.0 Use Orm::emitter()->removeAllListeners(). Will be removed in 1.0.0. 66 | */ 67 | public static function removeListeners($event = null) 68 | { 69 | self::deprecationNotice(__METHOD__, "Orm::emitter()->removeAllListeners()"); 70 | return Orm::emitter()->removeAllListeners($event); 71 | } 72 | 73 | /** 74 | * @deprecated 0.9.0 Use Orm::emitter()->removeListener(). Will be removed in 1.0.0. 75 | */ 76 | public static function removeListener($event, $listener) 77 | { 78 | self::deprecationNotice(__METHOD__, "Orm::emitter()->removeListener()"); 79 | return Orm::emitter()->removeListener($event, $listener); 80 | } 81 | 82 | private static function deprecationNotice($method, $new) 83 | { 84 | $msg = "Method $method is deprecated and will be removed."; 85 | $msg .= " Please use $new instead."; 86 | trigger_error($msg, E_USER_WARNING); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Phormium/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 42 | $this->filters = $filters; 43 | } 44 | 45 | /** 46 | * Returns a new instance of CompositeFilter with the given filter added to 47 | * existing $filters. 48 | * 49 | * Does not mutate the object. 50 | * 51 | * @param Filter $filter The filter to add. 52 | * 53 | * @return CompositeFilter 54 | */ 55 | public function withAdded(Filter $filter) 56 | { 57 | $operation = $this->operation(); 58 | $filters = $this->filters(); 59 | $filters[] = $filter; 60 | 61 | return new CompositeFilter($operation, $filters); 62 | } 63 | 64 | public function filters() 65 | { 66 | return $this->filters; 67 | } 68 | 69 | public function operation() 70 | { 71 | return $this->operation; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Phormium/Filter/Filter.php: -------------------------------------------------------------------------------- 1 | filter(). 72 | * 73 | * Here are the possibilities: 74 | * 75 | * 1. One argument given 76 | * 77 | * a) If it's a Filter object, just return it as-is. 78 | * e.g. `->filter(new Filter(...))` 79 | * 80 | * b) If it's an array, use it to construct a ColumnFilter. 81 | * e.g. `->filter(['foo', 'isnull']) 82 | * 83 | * c) If it's a string, use it to construct a RawFilter. 84 | * e.g. `->filter('foo = lower(bar)')` 85 | * 86 | * 2. Two arguments given 87 | * 88 | * a) If both are strings, use them to construct a ColumnFilter. 89 | * e.g. `->filter('foo', 'isnull') 90 | * 91 | * b) If one is string and the other an array, use it to construct a 92 | * Raw filter (first is SQL filter, the second is arguments). 93 | * e.g. `->filter('foo = concat(?, ?)', ['bar', 'baz']) 94 | * 95 | * 3. Three arguments given 96 | * 97 | * a) Use them to construct a ColumnFilter. 98 | * e.g. `->filter('foo', '=', 'bar') 99 | * 100 | * @return Phormium\Filter\Filter 101 | * @param [type] $args [description] 102 | * @return [type] [description] 103 | */ 104 | public static function factory(...$args) 105 | { 106 | $count = count($args); 107 | 108 | if ($count === 1) { 109 | $arg = $args[0]; 110 | 111 | if ($arg instanceof Filter) { 112 | return $arg; 113 | } elseif (is_array($arg)) { 114 | return ColumnFilter::fromArray($arg); 115 | } elseif (is_string($arg)) { 116 | return new RawFilter($arg); 117 | } 118 | } elseif ($count === 2) { 119 | if (is_string($args[0])) { 120 | if (is_string($args[1])) { 121 | return ColumnFilter::fromArray($args); 122 | } elseif (is_array($args[1])) { 123 | return new RawFilter($args[0], $args[1]); 124 | } 125 | } 126 | } elseif ($count === 3) { 127 | return ColumnFilter::fromArray($args); 128 | } 129 | 130 | throw new InvalidQueryException("Invalid filter arguments."); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Phormium/Filter/RawFilter.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 18 | $this->arguments = $arguments; 19 | } 20 | 21 | public function condition() 22 | { 23 | return $this->condition; 24 | } 25 | 26 | public function arguments() 27 | { 28 | return $this->arguments; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Phormium/Helper/Assert.php: -------------------------------------------------------------------------------- 1 | 0 && ( 18 | ctype_digit($value) || ( 19 | $value[0] == '-' && 20 | ctype_digit(substr($value, 1)) 21 | ) 22 | ) 23 | ); 24 | } 25 | 26 | /** 27 | * Checks whether the given value is a positive integer or a string 28 | * containing one. 29 | */ 30 | public static function isPositiveInteger($value) 31 | { 32 | return (is_int($value) && $value >= 0) || 33 | (is_string($value) && ctype_digit($value)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Phormium/Helper/Json.php: -------------------------------------------------------------------------------- 1 | table = $table; 31 | $this->database = $database; 32 | $this->class = $class; 33 | $this->columns = $columns; 34 | $this->pk = $pk; 35 | $this->nonPK = $nonPK; 36 | } 37 | 38 | public function getTable() 39 | { 40 | return $this->table; 41 | } 42 | 43 | public function getDatabase() 44 | { 45 | return $this->database; 46 | } 47 | 48 | public function getClass() 49 | { 50 | return $this->class; 51 | } 52 | 53 | public function getColumns() 54 | { 55 | return $this->columns; 56 | } 57 | 58 | public function getPkColumns() 59 | { 60 | return $this->pk; 61 | } 62 | 63 | public function getNonPkColumns() 64 | { 65 | return $this->nonPK; 66 | } 67 | 68 | public function columnExists($name) 69 | { 70 | return in_array($name, $this->columns); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Phormium/MetaBuilder.php: -------------------------------------------------------------------------------- 1 | checkModel($class); 31 | 32 | // Fetch user-defined model meta-data 33 | $_meta = call_user_func([$class, 'getRawMeta']); 34 | if (!is_array($_meta)) { 35 | throw new InvalidModelException("Invalid $class::\$_meta. Not an array."); 36 | } 37 | 38 | // Construct the Meta 39 | $database = $this->getDatabase($class, $_meta); 40 | $table = $this->getTable($class, $_meta); 41 | $columns = $this->getColumns($class); 42 | $pk = $this->getPK($class, $_meta, $columns); 43 | $nonPK = $this->getNonPK($columns, $pk); 44 | 45 | return new Meta( 46 | $table, 47 | $database, 48 | $class, 49 | $columns, 50 | $pk, 51 | $nonPK 52 | ); 53 | } 54 | 55 | /** 56 | * Returns class' public properties which correspond to column names. 57 | * 58 | * @return array 59 | */ 60 | private function getColumns($class) 61 | { 62 | $columns = []; 63 | 64 | $rc = new ReflectionClass($class); 65 | $props = $rc->getProperties(ReflectionProperty::IS_PUBLIC); 66 | $columns = array_map(function (ReflectionProperty $prop) { 67 | return $prop->name; 68 | }, $props); 69 | 70 | if (empty($columns)) { 71 | throw new InvalidModelException("Model $class has no defined columns (public properties)."); 72 | } 73 | 74 | return $columns; 75 | } 76 | 77 | /** 78 | * Verifies that the given classname is a valid class which extends Model. 79 | * 80 | * @param string $class The name of the class to check. 81 | * 82 | * @throws InvalidArgumentException If this is not the case. 83 | */ 84 | private function checkModel($class) 85 | { 86 | if (!is_string($class)) { 87 | throw new InvalidModelException("Invalid model given"); 88 | } 89 | 90 | if (!class_exists($class)) { 91 | throw new InvalidModelException("Class \"$class\" does not exist."); 92 | } 93 | 94 | if (!is_subclass_of($class, Model::class)) { 95 | throw new InvalidModelException("Class \"$class\" is not a subclass of Phormium\\Model."); 96 | } 97 | } 98 | 99 | /** Extracts the primary key column(s). */ 100 | private function getPK($class, $meta, $columns) 101 | { 102 | // If the primary key is not defined 103 | if (!isset($meta['pk'])) { 104 | // If the model has an "id" field, use that as the PK 105 | $diff = array_diff(self::$defaultPK, $columns); 106 | if (empty($diff)) { 107 | return self::$defaultPK; 108 | } else { 109 | return null; 110 | } 111 | } 112 | 113 | if (is_string($meta['pk'])) { 114 | $pk = [$meta['pk']]; 115 | } elseif (is_array($meta['pk'])) { 116 | $pk = $meta['pk']; 117 | } else { 118 | throw new InvalidModelException("Invalid primary key given in $class::\$_meta. Not a string or array."); 119 | } 120 | 121 | // Check all PK columns exist 122 | $missing = array_diff($pk, $columns); 123 | if (!empty($missing)) { 124 | $missing = implode(",", $missing); 125 | throw new InvalidModelException("Invalid $class::\$_meta. Specified primary key column(s) do not exist: $missing"); 126 | } 127 | 128 | return $pk; 129 | } 130 | 131 | /** Extracts the non-primary key columns. */ 132 | private function getNonPK($columns, $pk) 133 | { 134 | if ($pk === null) { 135 | return $columns; 136 | } 137 | 138 | return array_values( 139 | array_filter($columns, function ($column) use ($pk) { 140 | return !in_array($column, $pk); 141 | }) 142 | ); 143 | } 144 | 145 | /** Extracts the database name from user given model metadata. */ 146 | private function getDatabase($class, $meta) 147 | { 148 | if (empty($meta['database'])) { 149 | throw new InvalidModelException("Invalid $class::\$_meta. Missing \"database\"."); 150 | } 151 | 152 | return $meta['database']; 153 | } 154 | 155 | /** Extracts the table name from user given model metadata. */ 156 | private function getTable($class, $meta) 157 | { 158 | if (empty($meta['table'])) { 159 | throw new InvalidModelException("Invalid $class::\$_meta. Missing \"table\"."); 160 | } 161 | 162 | return $meta['table']; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Phormium/Orm.php: -------------------------------------------------------------------------------- 1 | disconnectAll(); 44 | self::$container = null; 45 | } 46 | 47 | /** 48 | * Returns the event emitter. 49 | * 50 | * @return EventEmitter 51 | */ 52 | public static function emitter() 53 | { 54 | return self::container()->offsetGet('emitter'); 55 | } 56 | 57 | /** 58 | * Returns the database manager object. 59 | * 60 | * @return Database 61 | */ 62 | public static function database() 63 | { 64 | return self::container()->offsetGet('database'); 65 | } 66 | 67 | /** 68 | * Starts a global transaction. 69 | * 70 | * Shorthand for `Orm::database()->begin()`. 71 | */ 72 | public static function begin() 73 | { 74 | self::database()->begin(); 75 | } 76 | 77 | /** 78 | * Ends the global transaction by committing changes on all connections. 79 | * 80 | * Shorthand for `Orm::database()->commit()`. 81 | */ 82 | public static function commit() 83 | { 84 | self::database()->commit(); 85 | } 86 | 87 | /** 88 | * Ends the global transaction by rolling back changes on all connections. 89 | * 90 | * Shorthand for `Orm::database()->rollback()`. 91 | */ 92 | public static function rollback() 93 | { 94 | self::database()->rollback(); 95 | } 96 | 97 | /** 98 | * Executes given callback within a transaction. Rolls back if an 99 | * exception is thrown within the callback. 100 | */ 101 | public static function transaction(callable $callback) 102 | { 103 | return self::database()->transaction($callback); 104 | } 105 | 106 | /** 107 | * Returns the QueryBuilder for a given database driver. 108 | * 109 | * @return QueryBuilderInterface 110 | */ 111 | public static function getQueryBuilder($driver) 112 | { 113 | $cache = self::container()['query_builder.cache']; 114 | $factory = self::container()['query_builder.factory']; 115 | 116 | if (!isset($cache[$driver])) { 117 | $cache[$driver] = $factory->getQueryBuilder($driver); 118 | } 119 | 120 | return $cache[$driver]; 121 | } 122 | 123 | /** 124 | * For a given database, returns the driver name. 125 | * 126 | * @param string $database Name of the database. 127 | * @return string Driver name. 128 | */ 129 | private static function getDatabaseDriver($database) 130 | { 131 | $config = self::container()->offsetGet('config'); 132 | 133 | if (!isset($config['databases'][$database])) { 134 | throw new OrmException("Database [$database] is not configured."); 135 | } 136 | 137 | return $config['databases'][$database]['driver']; 138 | } 139 | 140 | /** 141 | * Returns the Query class for a given Model class (cached). 142 | * 143 | * @param string $class Model class name. 144 | * @return Query The corresponding Query class. 145 | */ 146 | public static function getQuery($class) 147 | { 148 | $cache = self::container()['query.cache']; 149 | 150 | if (!isset($cache[$class])) { 151 | $meta = self::getMeta($class); 152 | $database = $meta->getDatabase(); 153 | $driver = self::getDatabaseDriver($database); 154 | $queryBuilder = self::getQueryBuilder($driver); 155 | 156 | $cache[$class] = new Query($meta, $queryBuilder, self::database()); 157 | } 158 | 159 | return $cache[$class]; 160 | } 161 | 162 | /** 163 | * Returns the Meta class for a given Model class (cached). 164 | * 165 | * @param string $class Model class name. 166 | * @return Meta The corresponding Meta class. 167 | */ 168 | public static function getMeta($class) 169 | { 170 | $cache = self::container()['meta.cache']; 171 | $builder = self::container()['meta.builder']; 172 | 173 | if (!isset($cache[$class])) { 174 | $cache[$class] = $builder->build($class); 175 | } 176 | 177 | return $cache[$class]; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Phormium/Printer.php: -------------------------------------------------------------------------------- 1 | mb_internal_encoding('UTF-8'); 25 | * 26 | * @param array $array Input array. 27 | * @param array $return If set to true, the dump will be returned as string 28 | * instead of printing it. 29 | */ 30 | public function dump($input, $return = false) 31 | { 32 | if ($input instanceof QuerySet) { 33 | return $this->dumpQS($input, $return); 34 | } elseif (is_array($input)) { 35 | return $this->dumpArray($input, $return); 36 | } 37 | 38 | throw new OrmException("Invalid input for dump(): not array or QuerySet."); 39 | } 40 | 41 | /** Dump implementation for arrays. */ 42 | private function dumpArray(array $array, $return = false) 43 | { 44 | if (empty($array)) { 45 | return; 46 | } 47 | 48 | $firstRow = $array[0]; 49 | if (!is_array($firstRow)) { 50 | throw new OrmException("Invalid input for dump(): first element not an array."); 51 | } 52 | 53 | $columns = array_keys($firstRow); 54 | 55 | return $this->dumpData($array, $columns, $return); 56 | } 57 | 58 | /** Dump implementation for QuerySets. */ 59 | private function dumpQS(QuerySet $querySet, $return = false) 60 | { 61 | $data = $querySet->fetch(); 62 | 63 | if (empty($data)) { 64 | return; 65 | } 66 | 67 | $columns = $querySet->getMeta()->getColumns(); 68 | 69 | return $this->dumpData($data, $columns, $return); 70 | } 71 | 72 | private function dumpData($data, $columns, $return = false) 73 | { 74 | // Record column names lengths 75 | $lengths = []; 76 | foreach ($columns as $name) { 77 | $lengths[$name] = mb_strlen($name); 78 | } 79 | 80 | // Process data for display and record data lengths 81 | foreach ($data as &$item) { 82 | if ($item instanceof Model) { 83 | $item = $item->toArray(); 84 | } 85 | 86 | if (!is_array($item)) { 87 | throw new OrmException("Invalid input for dump(): element not an array or Model."); 88 | } 89 | 90 | foreach ($columns as $column) { 91 | $value = $this->prepareValue($item[$column]); 92 | $item[$column] = $value; 93 | 94 | if (mb_strlen($value) > $lengths[$column]) { 95 | $lengths[$column] = mb_strlen($value); 96 | } 97 | } 98 | } 99 | unset($item); 100 | 101 | // Determine total row length 102 | $totalLength = 0; 103 | foreach ($lengths as $len) { 104 | $totalLength += $len; 105 | } 106 | 107 | // Account for padding between columns 108 | $totalLength += self::COLUMN_PADDING * (count($columns) - 1); 109 | 110 | // Start outputting data 111 | $output = ""; 112 | 113 | // Print the titles 114 | foreach ($columns as $column) { 115 | $output .= $this->strpad($column, $lengths[$column]); 116 | $output .= str_repeat(" ", self::COLUMN_PADDING); 117 | } 118 | $output .= PHP_EOL; 119 | 120 | // Print the line under titles 121 | $output .= str_repeat("=", $totalLength) . PHP_EOL; 122 | 123 | // Print the rows 124 | foreach ($data as $model) { 125 | foreach ($model as $column => $value) { 126 | $output .= $this->strpad($value, $lengths[$column]); 127 | $output .= str_repeat(" ", self::COLUMN_PADDING); 128 | } 129 | $output .= PHP_EOL; 130 | } 131 | 132 | if ($return) { 133 | return $output; 134 | } 135 | 136 | echo $output; 137 | } 138 | 139 | /** 140 | * Replacement for strpad() which uses mb_* functions. 141 | */ 142 | private function strpad($value, $length) 143 | { 144 | $padLength = $length - mb_strlen($value); 145 | 146 | // Sanity check: $padLength can be sub-zero when incorrect 147 | // mb_internal_encoding is used. 148 | if ($padLength > 0) { 149 | return str_repeat(" ", $padLength) . $value; 150 | } else { 151 | return $value; 152 | } 153 | } 154 | 155 | /** 156 | * Makes sure the value is a string and trims it to MAX_LENGTH chars if 157 | * needed. 158 | */ 159 | private function prepareValue($value) 160 | { 161 | if (is_array($value)) { 162 | $value = implode(', ', $value); 163 | } 164 | 165 | $value = trim(strval($value)); 166 | 167 | // Trim to max allowed length 168 | if (mb_strlen($value) > self::COLUMN_MAX_LENGTH) { 169 | $value = mb_substr($value, 0, self::COLUMN_MAX_LENGTH - 3) . '...'; 170 | } 171 | 172 | return $value; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Phormium/Query/Aggregate.php: -------------------------------------------------------------------------------- 1 | type = $type; 43 | $this->column = $column; 44 | } 45 | 46 | // -- Accessors ------------------------------------------------------------ 47 | 48 | public function type() 49 | { 50 | return $this->type; 51 | } 52 | 53 | public function column() 54 | { 55 | return $this->column; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Phormium/Query/ColumnOrder.php: -------------------------------------------------------------------------------- 1 | column = $column; 48 | $this->direction = $direction; 49 | } 50 | 51 | // -- Accessors ------------------------------------------------------------ 52 | 53 | public function column() 54 | { 55 | return $this->column; 56 | } 57 | 58 | public function direction() 59 | { 60 | return $this->direction; 61 | } 62 | 63 | // -- Factories ------------------------------------------------------------ 64 | 65 | public static function asc($column) 66 | { 67 | return new ColumnOrder($column, ColumnOrder::ASCENDING); 68 | } 69 | 70 | public static function desc($column) 71 | { 72 | return new ColumnOrder($column, ColumnOrder::DESCENDING); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Phormium/Query/LimitOffset.php: -------------------------------------------------------------------------------- 1 | limit = $limit; 43 | $this->offset = $offset; 44 | } 45 | 46 | // -- Accessors ------------------------------------------------------------ 47 | 48 | public function limit() 49 | { 50 | return $this->limit; 51 | } 52 | 53 | public function offset() 54 | { 55 | return $this->offset; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Phormium/Query/OrderBy.php: -------------------------------------------------------------------------------- 1 | orders = $orders; 35 | } 36 | 37 | public function orders() 38 | { 39 | return $this->orders; 40 | } 41 | 42 | /** 43 | * Returns a new instance of OrderBy with the given ColumnOrder added onto 44 | * the $orders collection. 45 | * 46 | * @param ColumnOrder $order The order to add. 47 | * 48 | * @return OrderBy 49 | */ 50 | public function withAdded(ColumnOrder $order) 51 | { 52 | $orders = $this->orders; 53 | $orders[] = $order; 54 | 55 | return new OrderBy($orders); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Phormium/Query/QuerySegment.php: -------------------------------------------------------------------------------- 1 | query = $query; 31 | $this->args = $args; 32 | } 33 | 34 | public function query() 35 | { 36 | return $this->query; 37 | } 38 | 39 | public function args() 40 | { 41 | return $this->args; 42 | } 43 | 44 | /** 45 | * Combines two segments into a larger one. 46 | * 47 | * @param QuerySegment $other 48 | * @return QuerySegment 49 | */ 50 | public function combine(QuerySegment $other) 51 | { 52 | $query = trim($this->query() . " " . $other->query()); 53 | $args = array_merge($this->args, $other->args); 54 | 55 | return new QuerySegment($query, $args); 56 | } 57 | 58 | /** 59 | * Reduces an array of QuerySegments into one Query segment. 60 | * 61 | * @param QuerySegment[] $segments 62 | * 63 | * @return QuerySegment 64 | */ 65 | public static function reduce(array $segments) 66 | { 67 | $initial = new QuerySegment(); 68 | 69 | $reduceFn = function (QuerySegment $one, QuerySegment $other) { 70 | return $one->combine($other); 71 | }; 72 | 73 | return array_reduce($segments, $reduceFn, $initial); 74 | } 75 | 76 | 77 | /** 78 | * Implodes an array of QuerySegment by inserting a separator QuerySegment 79 | * between each two segments in the array, then reducing them. 80 | * 81 | * @param QuerySegment $separator 82 | * @param QuerySegment[] $segments 83 | * @return QuerySegment 84 | */ 85 | public static function implode(QuerySegment $separator, array $segments) 86 | { 87 | if (empty($segments)) { 88 | return new QuerySegment(); 89 | } 90 | 91 | if (count($segments) === 1) { 92 | return reset($segments); 93 | } 94 | 95 | $first = array_shift($segments); 96 | 97 | $imploded = [$first]; 98 | foreach ($segments as $segment) { 99 | $imploded[] = $separator; 100 | $imploded[] = $segment; 101 | } 102 | 103 | return self::reduce($imploded); 104 | } 105 | 106 | /** 107 | * Embraces the query in parenthesis, leaving the arguments unchanged. 108 | * 109 | * Given "foo AND bar", returns "(foo AND bar)". 110 | * 111 | * @param QuerySegment $segment Segment to embrace. 112 | * 113 | * @return QuerySegment Embraced segment. 114 | */ 115 | public static function embrace(QuerySegment $segment) 116 | { 117 | return new QuerySegment("(" . $segment->query() . ")", $segment->args()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/Common/FilterRenderer.php: -------------------------------------------------------------------------------- 1 | quoter = $quoter; 22 | } 23 | 24 | public function renderFilter(Filter $filter) 25 | { 26 | if ($filter instanceof ColumnFilter) { 27 | return $this->renderColumnFilter($filter); 28 | } 29 | 30 | if ($filter instanceof CompositeFilter) { 31 | return $this->renderCompositeFilter($filter); 32 | } 33 | 34 | if ($filter instanceof RawFilter) { 35 | return $this->renderRawFilter($filter); 36 | } 37 | 38 | throw new OrmException("Unknown filter class: " . get_class($filter)); 39 | } 40 | 41 | public function renderRawFilter(RawFilter $filter) 42 | { 43 | return new QuerySegment($filter->condition(), $filter->arguments()); 44 | } 45 | 46 | public function renderCompositeFilter(CompositeFilter $filter) 47 | { 48 | $subFilters = $filter->filters(); 49 | 50 | if (empty($subFilters)) { 51 | throw new OrmException("Canot render composite filter. No filters defined."); 52 | } 53 | 54 | if (count($subFilters) === 1) { 55 | return $this->renderFilter($subFilters[0]); 56 | } 57 | 58 | $segments = array_map([$this, "renderFilter"], $subFilters); 59 | 60 | $separator = new QuerySegment($filter->operation()); 61 | $imploded = QuerySegment::implode($separator, $segments); 62 | 63 | return QuerySegment::embrace($imploded); 64 | } 65 | 66 | /** 67 | * Renders a WHERE condition for the given filter. 68 | */ 69 | public function renderColumnFilter(ColumnFilter $filter) 70 | { 71 | $column = $this->quoter->quote($filter->column()); 72 | $operation = $filter->operation(); 73 | $value = $filter->value(); 74 | 75 | switch ($operation) { 76 | case ColumnFilter::OP_EQUALS: 77 | return is_null($value) ? 78 | $this->renderIsNull($column) : 79 | $this->renderSimple($column, $operation, $value); 80 | 81 | case ColumnFilter::OP_NOT_EQUALS: 82 | case ColumnFilter::OP_NOT_EQUALS_ALT: 83 | return is_null($value) ? 84 | $this->renderNotNull($column) : 85 | $this->renderSimple($column, $operation, $value); 86 | 87 | case ColumnFilter::OP_LIKE: 88 | case ColumnFilter::OP_NOT_LIKE: 89 | case ColumnFilter::OP_GREATER: 90 | case ColumnFilter::OP_GREATER_OR_EQUAL: 91 | case ColumnFilter::OP_LESSER: 92 | case ColumnFilter::OP_LESSER_OR_EQUAL: 93 | return $this->renderSimple($column, $operation, $value); 94 | 95 | case ColumnFilter::OP_LIKE_CASE_INSENSITIVE: 96 | return $this->renderLikeCaseInsensitive($column, $operation, $value); 97 | 98 | case ColumnFilter::OP_IN: 99 | return $this->renderIn($column, $operation, $value); 100 | 101 | case ColumnFilter::OP_NOT_IN: 102 | return $this->renderNotIn($column, $operation, $value); 103 | 104 | case ColumnFilter::OP_IS_NULL: 105 | return $this->renderIsNull($column); 106 | 107 | case ColumnFilter::OP_NOT_NULL: 108 | case ColumnFilter::OP_NOT_NULL_ALT: 109 | return $this->renderNotNull($column); 110 | 111 | case ColumnFilter::OP_BETWEEN: 112 | return $this->renderBetween($column, $operation, $value); 113 | 114 | default: 115 | throw new OrmException("Unknown filter operation [{$operation}]."); 116 | } 117 | } 118 | 119 | /** 120 | * Renders a simple condition which can be expressed as: 121 | * 122 | */ 123 | private function renderSimple($column, $operation, $value) 124 | { 125 | $where = "{$column} {$operation} ?"; 126 | 127 | return new QuerySegment($where, [$value]); 128 | } 129 | 130 | private function renderBetween($column, $operation, $values) 131 | { 132 | $where = "$column BETWEEN ? AND ?"; 133 | 134 | return new QuerySegment($where, $values); 135 | } 136 | 137 | private function renderIn($column, $operation, $values) 138 | { 139 | $placeholders = array_fill(0, count($values), '?'); 140 | $where = "$column IN (" . implode(', ', $placeholders) . ")"; 141 | 142 | return new QuerySegment($where, $values); 143 | } 144 | 145 | private function renderLikeCaseInsensitive($column, $operation, $value) 146 | { 147 | $where = "lower($column) LIKE lower(?)"; 148 | 149 | return new QuerySegment($where, [$value]); 150 | } 151 | 152 | private function renderNotIn($column, $operation, $values) 153 | { 154 | $placeholders = array_fill(0, count($values), '?'); 155 | $where = "$column NOT IN (" . implode(', ', $placeholders) . ")"; 156 | 157 | return new QuerySegment($where, $values); 158 | } 159 | 160 | private function renderIsNull($column) 161 | { 162 | return new QuerySegment("$column IS NULL"); 163 | } 164 | 165 | private function renderNotNull($column) 166 | { 167 | return new QuerySegment("$column IS NOT NULL"); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/Common/Quoter.php: -------------------------------------------------------------------------------- 1 | left . trim($name) . $this->right; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/Mysql/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | limit(); 21 | $offset = $limitOffset->offset(); 22 | 23 | $limitSegment = isset($limit) ? 24 | new QuerySegment("LIMIT $limit") : 25 | new QuerySegment(); 26 | 27 | $offsetSegment = isset($offset) ? 28 | new QuerySegment("OFFSET $offset") : 29 | new QuerySegment(); 30 | 31 | return $limitSegment->combine($offsetSegment); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/Mysql/Quoter.php: -------------------------------------------------------------------------------- 1 | quoter->quote($column); 15 | return new QuerySegment("RETURNING $column"); 16 | } 17 | 18 | return new QuerySegment(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/QueryBuilderFactory.php: -------------------------------------------------------------------------------- 1 | getClass("Quoter", $driver); 19 | $filterRendererClass = $this->getClass("FilterRenderer", $driver); 20 | $queryBuilderClass = $this->getClass("QueryBuilder", $driver); 21 | 22 | $quoter = new $quoterClass(); 23 | $filterRenderer = new $filterRendererClass($quoter); 24 | return new $queryBuilderClass($quoter, $filterRenderer); 25 | } 26 | 27 | private function getClass($className, $driver) 28 | { 29 | $driverNS = ucfirst($driver); 30 | 31 | $driverClass = __NAMESPACE__ . "\\$driverNS\\$className"; 32 | $commonClass = __NAMESPACE__ . "\\Common\\$className"; 33 | 34 | return class_exists($driverClass) ? $driverClass : $commonClass; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Phormium/QueryBuilder/QueryBuilderInterface.php: -------------------------------------------------------------------------------- 1 | exec($sql); 22 | unset($pdo); 23 | -------------------------------------------------------------------------------- /tests/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "testdb": { 4 | "dsn": "sqlite:tmp/test.db", 5 | "username": "", 6 | "password": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/integration/AggregateTest.php: -------------------------------------------------------------------------------- 1 | filter('tradedate', '=', $tradedate); 21 | 22 | // Delete any existing trades for today 23 | $qs->delete(); 24 | 25 | // Create trades with random prices and quantitities 26 | $prices = []; 27 | $quantities = []; 28 | 29 | foreach(range(1, $count) as $tradeno) { 30 | $price = rand(100, 100000) / 100;; 31 | $quantity = rand(1, 10000); 32 | 33 | $t = new Trade(); 34 | $t->merge(compact('tradedate', 'tradeno', 'price', 'quantity')); 35 | $t->insert(); 36 | 37 | $prices[] = $price; 38 | $quantities[] = $quantity; 39 | } 40 | 41 | // Calculate expected values 42 | $avgPrice = array_sum($prices) / count($prices); 43 | $maxPrice = max($prices); 44 | $minPrice = min($prices); 45 | $sumPrice = array_sum($prices); 46 | 47 | $avgQuantity = array_sum($quantities) / count($quantities); 48 | $maxQuantity = max($quantities); 49 | $minQuantity = min($quantities); 50 | $sumQuantity = array_sum($quantities); 51 | 52 | $this->assertSame($count, $qs->count()); 53 | 54 | $this->assertEquals($avgPrice, $qs->avg('price')); 55 | $this->assertEquals($minPrice, $qs->min('price')); 56 | $this->assertEquals($avgPrice, $qs->avg('price')); 57 | $this->assertEquals($sumPrice, $qs->sum('price')); 58 | 59 | $this->assertEquals($avgQuantity, $qs->avg('quantity')); 60 | $this->assertEquals($minQuantity, $qs->min('quantity')); 61 | $this->assertEquals($avgQuantity, $qs->avg('quantity')); 62 | $this->assertEquals($sumQuantity, $qs->sum('quantity')); 63 | } 64 | 65 | /** 66 | * @expectedException Phormium\Exception\InvalidQueryException 67 | * @expectedExceptionMessage Error forming aggregate query. Column [xxx] does not exist in table [trade]. 68 | */ 69 | public function testInvalidColumn() 70 | { 71 | Trade::objects()->avg('xxx'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/integration/DbTest.php: -------------------------------------------------------------------------------- 1 | filter('name', 'ilike', 'pero'); 23 | 24 | $qs->delete(); 25 | $this->assertFalse($qs->exists()); 26 | 27 | Person::fromArray(['name' => "PERO"])->insert(); 28 | Person::fromArray(['name' => "pero"])->insert(); 29 | Person::fromArray(['name' => "Pero"])->insert(); 30 | Person::fromArray(['name' => "pERO"])->insert(); 31 | 32 | $this->assertSame(4, $qs->count()); 33 | $this->assertCount(4, $qs->fetch()); 34 | } 35 | 36 | function testRawFilter() 37 | { 38 | $condition = "lower(name) = ?"; 39 | $arguments = ['foo']; 40 | 41 | $qs = Person::objects()->filter($condition, $arguments); 42 | 43 | $filter1 = $qs->getFilter(); 44 | $expected = CompositeFilter::class; 45 | $this->assertInstanceOf($expected, $filter1); 46 | $this->assertSame('AND', $filter1->operation()); 47 | 48 | $filters = $filter1->filters(); 49 | $this->assertCount(1, $filters); 50 | 51 | $filter2 = $filters[0]; 52 | $expected = RawFilter::class; 53 | $this->assertInstanceOf($expected, $filter2); 54 | 55 | $this->assertSame($condition, $filter2->condition()); 56 | $this->assertSame($arguments, $filter2->arguments()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/integration/ModelRelationsTraitTest.php: -------------------------------------------------------------------------------- 1 | 'Udo Dirkschneider']); 23 | self::$person->save(); 24 | } 25 | 26 | public function testGuessableRelation() 27 | { 28 | $pid = self::$person->id; 29 | 30 | // Contacts are linked to person via a guessable foreign key name 31 | // (person_id) 32 | $c1 = Contact::fromArray(['person_id' => $pid, "value" => "Contact #1"]); 33 | $c2 = Contact::fromArray(['person_id' => $pid, "value" => "Contact #2"]); 34 | $c3 = Contact::fromArray(['person_id' => $pid, "value" => "Contact #3"]); 35 | 36 | $c1->save(); 37 | $c2->save(); 38 | $c3->save(); 39 | 40 | $contacts = self::$person->hasChildren(Contact::class); 41 | $this->assertInstanceOf(QuerySet::class, $contacts); 42 | 43 | $actual = $contacts->fetch(); 44 | $expected = [$c1, $c2, $c3]; 45 | $this->assertEquals($expected, $actual); 46 | 47 | $p1 = $c1->hasParent(Person::class)->single(); 48 | $p2 = $c2->hasParent(Person::class)->single(); 49 | $p3 = $c3->hasParent(Person::class)->single(); 50 | 51 | $this->assertEquals(self::$person, $p1); 52 | $this->assertEquals(self::$person, $p2); 53 | $this->assertEquals(self::$person, $p3); 54 | } 55 | 56 | public function testUnguessableRelation() 57 | { 58 | $pid = self::$person->id; 59 | 60 | // Asset is similar to contact, but has a non-guessable foreign key name 61 | // (owner_id) 62 | $a1 = Asset::fromArray(['owner_id' => $pid, "value" => "Asset #1"]); 63 | $a2 = Asset::fromArray(['owner_id' => $pid, "value" => "Asset #2"]); 64 | $a3 = Asset::fromArray(['owner_id' => $pid, "value" => "Asset #3"]); 65 | 66 | $a1->save(); 67 | $a2->save(); 68 | $a3->save(); 69 | 70 | $assets = self::$person->hasChildren(Asset::class, "owner_id"); 71 | $this->assertInstanceOf(QuerySet::class, $assets); 72 | 73 | $actual = $assets->fetch(); 74 | $expected = [$a1, $a2, $a3]; 75 | $this->assertEquals($expected, $actual); 76 | 77 | $p1 = $a1->hasParent(Person::class, "owner_id")->single(); 78 | $p2 = $a2->hasParent(Person::class, "owner_id")->single(); 79 | $p3 = $a3->hasParent(Person::class, "owner_id")->single(); 80 | 81 | $this->assertEquals(self::$person, $p1); 82 | $this->assertEquals(self::$person, $p2); 83 | $this->assertEquals(self::$person, $p3); 84 | } 85 | 86 | /** 87 | * @expectedException Phormium\Exception\InvalidRelationException 88 | * @expectedExceptionMessage Model class "foo" does not exist 89 | */ 90 | public function testInvalidModel1() 91 | { 92 | // Class does not exist 93 | self::$person->hasChildren("foo"); 94 | } 95 | 96 | /** 97 | * @expectedException Phormium\Exception\InvalidRelationException 98 | * @expectedExceptionMessage Given class "DateTime" is not a subclass of Phormium\Model 99 | */ 100 | public function testInvalidModel2() 101 | { 102 | // Class exists but is not a model 103 | self::$person->hasChildren("DateTime"); 104 | } 105 | 106 | /** 107 | * @expectedException Phormium\Exception\InvalidRelationException 108 | * @expectedExceptionMessage Empty key given 109 | */ 110 | public function testInvalidKey1() 111 | { 112 | // Empty key 113 | self::$person->hasChildren(Contact::class, []); 114 | } 115 | 116 | /** 117 | * @expectedException Phormium\Exception\InvalidRelationException 118 | * @expectedExceptionMessage Invalid key type: "object". Expected string or array. 119 | */ 120 | public function testInvalidKey2() 121 | { 122 | // Key is a class instead of string or array 123 | self::$person->hasChildren(Contact::class, new Contact()); 124 | } 125 | 126 | /** 127 | * @expectedException Phormium\Exception\InvalidRelationException 128 | * @expectedExceptionMessage Property "foo" does not exist 129 | */ 130 | public function testInvalidKey3() 131 | { 132 | // Property does not exist 133 | self::$person->hasChildren(Contact::class, "foo"); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/integration/PrinterTest.php: -------------------------------------------------------------------------------- 1 | filter("name", "=", $name)->delete(); 25 | 26 | $person1 = Person::fromArray(["name" => $name, "income" => 100]); 27 | $person2 = Person::fromArray(["name" => $name, "income" => 200]); 28 | $person3 = Person::fromArray(["name" => $name, "income" => 300]); 29 | 30 | $person1->save(); $id1 = $person1->id; 31 | $person2->save(); $id2 = $person2->id; 32 | $person3->save(); $id3 = $person3->id; 33 | 34 | $actual = Person::objects()->filter("name", "=", $name)->dump(true); 35 | $lines = explode(PHP_EOL, $actual); 36 | 37 | $this->assertRegExp("/^\\s*id\\s+name\\s+email\\s+birthday\\s+created\\s+income\\s+is_cool\\s*$/", $lines[0]); 38 | $this->assertRegExp("/^=+$/", $lines[1]); 39 | $this->assertRegExp("/^\\s*$id1\\s+Freddy Mercury\\s+100(.00)?\\s*$/", $lines[2]); 40 | $this->assertRegExp("/^\\s*$id2\\s+Freddy Mercury\\s+200(.00)?\\s*$/", $lines[3]); 41 | $this->assertRegExp("/^\\s*$id3\\s+Freddy Mercury\\s+300(.00)?\\s*$/", $lines[4]); 42 | } 43 | 44 | public function testDumpArrayReturn() 45 | { 46 | $name = "Freddy Mercury"; 47 | 48 | $data = [ 49 | ["id" => 1, "name" => $name, "email" => "freddy@queen.org", "income" => 100], 50 | ["id" => 2, "name" => $name, "email" => "freddy@queen.org", "income" => 200], 51 | ["id" => 3, "name" => $name, "email" => "freddy@queen.org", "income" => 300], 52 | ]; 53 | 54 | $printer = new Printer(); 55 | $actual = $printer->dump($data, true); 56 | $lines = explode(PHP_EOL, $actual); 57 | 58 | $this->assertRegExp("/^\\s*id\\s+name\\s+email\\s+income\\s*$/", $lines[0]); 59 | $this->assertRegExp("/^=+$/", $lines[1]); 60 | $this->assertRegExp("/^\\s*1\\s+Freddy Mercury\\s+freddy@queen.org\\s+100(.00)?\\s*$/", $lines[2]); 61 | $this->assertRegExp("/^\\s*2\\s+Freddy Mercury\\s+freddy@queen.org\\s+200(.00)?\\s*$/", $lines[3]); 62 | $this->assertRegExp("/^\\s*3\\s+Freddy Mercury\\s+freddy@queen.org\\s+300(.00)?\\s*$/", $lines[4]); 63 | } 64 | 65 | public function testDumpEcho() 66 | { 67 | $name = "Rob Halford"; 68 | 69 | Person::objects()->filter("name", "=", $name)->delete(); 70 | 71 | $person1 = Person::fromArray(["name" => $name, "income" => 100]); 72 | $person2 = Person::fromArray(["name" => $name, "income" => 200]); 73 | $person3 = Person::fromArray(["name" => $name, "income" => 300]); 74 | 75 | $person1->save(); $id1 = $person1->id; 76 | $person2->save(); $id2 = $person2->id; 77 | $person3->save(); $id3 = $person3->id; 78 | 79 | ob_start(); 80 | Person::objects()->filter("name", "=", $name)->dump(); 81 | $actual = ob_get_clean(); 82 | 83 | $lines = explode(PHP_EOL, $actual); 84 | 85 | $this->assertRegExp("/^\\s*id\\s+name\\s+email\\s+birthday\\s+created\\s+income\\s+is_cool\\s*$/", $lines[0]); 86 | $this->assertRegExp("/^=+$/", $lines[1]); 87 | $this->assertRegExp("/^\\s*$id1\\s+Rob Halford\\s+100(.00)?\\s*$/", $lines[2]); 88 | $this->assertRegExp("/^\\s*$id2\\s+Rob Halford\\s+200(.00)?\\s*$/", $lines[3]); 89 | $this->assertRegExp("/^\\s*$id3\\s+Rob Halford\\s+300(.00)?\\s*$/", $lines[4]); 90 | } 91 | 92 | public function testDumpEchoEmptyQS() 93 | { 94 | $name = "Rob Halford"; 95 | 96 | Person::objects()->filter("name", "=", $name)->delete(); 97 | 98 | ob_start(); 99 | Person::objects()->filter("name", "=", $name)->dump(); 100 | $actual = ob_get_clean(); 101 | 102 | $this->assertSame("", $actual); 103 | } 104 | 105 | public function testDumpEchoEmptyArray() 106 | { 107 | ob_start(); 108 | $printer = new Printer(); 109 | $printer->dump([]); 110 | $actual = ob_get_clean(); 111 | 112 | $this->assertSame("", $actual); 113 | } 114 | } -------------------------------------------------------------------------------- /tests/models/Asset.php: -------------------------------------------------------------------------------- 1 | 'testdb', 14 | 'table' => 'asset', 15 | 'pk' => 'id' 16 | ]; 17 | 18 | public $id; 19 | public $owner_id; 20 | public $value; 21 | } 22 | -------------------------------------------------------------------------------- /tests/models/Contact.php: -------------------------------------------------------------------------------- 1 | 'testdb', 11 | 'table' => 'contact', 12 | 'pk' => 'id' 13 | ]; 14 | 15 | public $id; 16 | public $person_id; 17 | public $value; 18 | } 19 | -------------------------------------------------------------------------------- /tests/models/InvalidModel1.php: -------------------------------------------------------------------------------- 1 | 'database1', 12 | 'table' => 'invalid_model_2' 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /tests/models/Model1.php: -------------------------------------------------------------------------------- 1 | 'database1', 12 | 'table' => 'model1' 13 | ]; 14 | 15 | public $id; 16 | public $foo; 17 | public $bar; 18 | public $baz; 19 | } 20 | -------------------------------------------------------------------------------- /tests/models/Model2.php: -------------------------------------------------------------------------------- 1 | 'foo', 12 | 'database' => 'database1', 13 | 'table' => 'model2' 14 | ]; 15 | 16 | public $foo; 17 | public $bar; 18 | public $baz; 19 | } 20 | -------------------------------------------------------------------------------- /tests/models/NotModel.php: -------------------------------------------------------------------------------- 1 | 'testdb', 9 | 'table' => 'person', 10 | 'pk' => 'id' 11 | ]; 12 | 13 | public $id; 14 | public $name; 15 | public $email; 16 | public $birthday; 17 | public $created; 18 | public $income; 19 | public $is_cool; 20 | } 21 | -------------------------------------------------------------------------------- /tests/models/PkLess.php: -------------------------------------------------------------------------------- 1 | 'testdb', 12 | 'table' => 'pkless', 13 | ]; 14 | 15 | public $foo; 16 | public $bar; 17 | public $baz; 18 | } 19 | -------------------------------------------------------------------------------- /tests/models/Trade.php: -------------------------------------------------------------------------------- 1 | 'testdb', 13 | 'table' => 'trade', 14 | 'pk' => ['tradedate', 'tradeno'] 15 | ]; 16 | 17 | public $tradedate; 18 | public $tradeno; 19 | public $price; 20 | public $quantity; 21 | } 22 | -------------------------------------------------------------------------------- /tests/performance/README.md: -------------------------------------------------------------------------------- 1 | Phormium performance test suite 2 | =============================== 3 | 4 | Create a test database called `phtest`: 5 | 6 | ``` 7 | $ createdb -U postgres phtest 8 | ``` 9 | 10 | Run the test: 11 | 12 | ``` 13 | $ php performance.php 14 | ``` 15 | 16 | Results will be saved in JSON in results folder. 17 | -------------------------------------------------------------------------------- /tests/performance/functions.php: -------------------------------------------------------------------------------- 1 | 'test', 7 | 'table' => 'city', 8 | 'pk' => 'id' 9 | ]; 10 | 11 | public $id; 12 | public $name; 13 | public $countrycode; 14 | public $district; 15 | public $population; 16 | } 17 | 18 | class Country extends Phormium\Model 19 | { 20 | protected static $_meta = [ 21 | 'database' => 'test', 22 | 'table' => 'country', 23 | 'pk' => 'code' 24 | ]; 25 | 26 | public $code; 27 | public $name; 28 | public $continent; 29 | public $region; 30 | public $surfacearea; 31 | public $indepyear; 32 | public $population; 33 | public $lifeexpectancy; 34 | public $gnp; 35 | public $gnpold; 36 | public $localname; 37 | public $governmentform; 38 | public $headofstate; 39 | public $capital; 40 | public $code2; 41 | } -------------------------------------------------------------------------------- /tests/performance/performance.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'test' => [ 16 | 'dsn' => 'pgsql:host=localhost;dbname=phtest', 17 | 'username' => 'postgres', 18 | 'password' => '' 19 | ] 20 | ] 21 | ]); 22 | 23 | echo "Phormium performance test suite\n"; 24 | echo "===============================\n"; 25 | 26 | echo "Reseting database.\n"; 27 | `psql --quiet --username postgres --dbname=phtest < world.sql`; 28 | 29 | echo "-------------------------------\n"; 30 | 31 | // ---------------------------------------------- 32 | 33 | start("Select all"); 34 | repeat(20, function() { 35 | City::all(); 36 | }); 37 | finish(); 38 | 39 | // ---------------------------------------------- 40 | 41 | $cities = City::all(); 42 | 43 | start("Select each by ID"); 44 | foreach($cities as $city) { 45 | City::get($city->id); 46 | } 47 | finish(); 48 | 49 | // ---------------------------------------------- 50 | 51 | start("Select each by name"); 52 | foreach($cities as $city) { 53 | City::get($city->id); 54 | } 55 | finish(); 56 | 57 | // ----------------------------------------------z 58 | 59 | start("Update all"); 60 | repeat(20, function() { 61 | City::objects()->update([ 62 | 'population' => 1 63 | ]); 64 | }); 65 | finish(); 66 | 67 | // ----------------------------------------------z 68 | 69 | $cities = City::all(); 70 | 71 | start("Update each (update)"); 72 | foreach($cities as $city) { 73 | $city->population += 1; 74 | $city->update(); 75 | } 76 | finish(); 77 | 78 | // ----------------------------------------------z 79 | 80 | $cities = City::all(); 81 | 82 | start("Update each (save)"); 83 | foreach($cities as $city) { 84 | $city->population += 1; 85 | $city->save(); 86 | } 87 | finish(); 88 | 89 | // ---------------------------------------------- 90 | 91 | save(); 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/performance/results/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /tests/performance/world.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/phormium/ab8776ca2da9bc0e804be88b92d1e608ba75b89c/tests/performance/world.sql -------------------------------------------------------------------------------- /tests/travis/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Current foder 4 | SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd) 5 | 6 | composer install 7 | 8 | mkdir tmp 9 | 10 | if [ $DB = "mysql" ] 11 | then 12 | mysql < $SCRIPT_DIR/mysql/setup.sql 13 | fi 14 | 15 | if [ $DB = "sqlite" ] 16 | then 17 | sqlite3 tmp/test.db < $SCRIPT_DIR/sqlite/setup.sql 18 | fi 19 | 20 | if [ $DB = "postgres" ] 21 | then 22 | dropdb -U postgres --if-exists phormium_tests 23 | createdb -U postgres phormium_tests 24 | psql -U postgres -d phormium_tests -f $SCRIPT_DIR/postgres/setup.sql 25 | fi 26 | -------------------------------------------------------------------------------- /tests/travis/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Phormium\\Tests', __DIR__ . '/../'); 5 | -------------------------------------------------------------------------------- /tests/travis/mysql/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "testdb": { 4 | "dsn": "mysql:host=localhost;dbname=phormium_tests", 5 | "username": "root", 6 | "password": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/travis/mysql/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../../../tests/unit 5 | 6 | 7 | ../../../tests/integration 8 | 9 | 10 | 11 | Phormium 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/travis/mysql/setup.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS phormium_tests; 2 | CREATE DATABASE phormium_tests; 3 | 4 | USE phormium_tests; 5 | 6 | CREATE TABLE person ( 7 | id INTEGER NOT NULL AUTO_INCREMENT, 8 | name VARCHAR(255) NOT NULL, 9 | email VARCHAR(255), 10 | birthday DATE, 11 | created DATETIME, 12 | income DECIMAL(10,2), 13 | is_cool BOOLEAN, 14 | PRIMARY KEY (id) 15 | ); 16 | 17 | DROP TABLE IF EXISTS contact; 18 | CREATE TABLE contact( 19 | id INTEGER NOT NULL AUTO_INCREMENT, 20 | person_id INTEGER NOT NULL, 21 | value VARCHAR(255), 22 | PRIMARY KEY (id), 23 | FOREIGN KEY (person_id) REFERENCES person(id) 24 | ); 25 | 26 | DROP TABLE IF EXISTS asset; 27 | CREATE TABLE asset( 28 | id INTEGER NOT NULL AUTO_INCREMENT, 29 | owner_id INTEGER NOT NULL, 30 | value VARCHAR(255), 31 | PRIMARY KEY (id), 32 | FOREIGN KEY (owner_id) REFERENCES person(id) 33 | ); 34 | 35 | CREATE TABLE trade( 36 | tradedate DATE NOT NULL, 37 | tradeno INTEGER NOT NULL, 38 | price DECIMAL(10,2), 39 | quantity INTEGER, 40 | PRIMARY KEY(tradedate, tradeno) 41 | ); 42 | 43 | CREATE TABLE pkless ( 44 | foo VARCHAR(20), 45 | bar VARCHAR(20), 46 | baz VARCHAR(20) 47 | ); 48 | -------------------------------------------------------------------------------- /tests/travis/postgres/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "testdb": { 4 | "dsn": "pgsql:host=localhost;dbname=phormium_tests", 5 | "username": "postgres", 6 | "password": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/travis/postgres/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../../../tests/unit 5 | 6 | 7 | ../../../tests/integration 8 | 9 | 10 | 11 | Phormium 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/travis/postgres/setup.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS person; 2 | CREATE TABLE person ( 3 | id SERIAL, 4 | name VARCHAR(255) NOT NULL, 5 | email VARCHAR(255), 6 | birthday DATE, 7 | created TIMESTAMP, 8 | income DECIMAL(10,2), 9 | is_cool BOOLEAN, 10 | PRIMARY KEY (id) 11 | ); 12 | 13 | DROP TABLE IF EXISTS contact; 14 | CREATE TABLE contact( 15 | id SERIAL, 16 | person_id INTEGER NOT NULL, 17 | value VARCHAR(255), 18 | PRIMARY KEY (id), 19 | FOREIGN KEY (person_id) REFERENCES person(id) 20 | ); 21 | 22 | DROP TABLE IF EXISTS asset; 23 | CREATE TABLE asset( 24 | id SERIAL, 25 | owner_id INTEGER NOT NULL, 26 | value VARCHAR(255), 27 | PRIMARY KEY (id), 28 | FOREIGN KEY (owner_id) REFERENCES person(id) 29 | ); 30 | 31 | DROP TABLE IF EXISTS trade; 32 | CREATE TABLE trade( 33 | tradedate DATE NOT NULL, 34 | tradeno INTEGER NOT NULL, 35 | price DECIMAL(10,2), 36 | quantity INTEGER, 37 | PRIMARY KEY(tradedate, tradeno) 38 | ); 39 | 40 | DROP TABLE IF EXISTS pkless; 41 | CREATE TABLE pkless ( 42 | foo VARCHAR(20), 43 | bar VARCHAR(20), 44 | baz VARCHAR(20) 45 | ); 46 | -------------------------------------------------------------------------------- /tests/travis/sqlite/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "testdb": { 4 | "dsn": "sqlite:tmp/test.db", 5 | "username": "", 6 | "password": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/travis/sqlite/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../../../tests/unit 5 | 6 | 7 | ../../../tests/integration 8 | 9 | 10 | 11 | Phormium 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/travis/sqlite/setup.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS person; 2 | CREATE TABLE person ( 3 | id INTEGER PRIMARY KEY, 4 | name VARCHAR(255) NOT NULL, 5 | email VARCHAR(255), 6 | birthday DATE, 7 | created DATETIME, 8 | income DECIMAL(10,2), 9 | is_cool BOOLEAN 10 | ); 11 | 12 | DROP TABLE IF EXISTS contact; 13 | CREATE TABLE contact( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | person_id INTEGER NOT NULL, 16 | value VARCHAR(255), 17 | FOREIGN KEY (person_id) REFERENCES person(id) 18 | ); 19 | 20 | DROP TABLE IF EXISTS asset; 21 | CREATE TABLE asset( 22 | id INTEGER PRIMARY KEY AUTOINCREMENT, 23 | owner_id INTEGER NOT NULL, 24 | value VARCHAR(255), 25 | FOREIGN KEY (owner_id) REFERENCES person(id) 26 | ); 27 | 28 | DROP TABLE IF EXISTS trade; 29 | CREATE TABLE trade( 30 | tradedate DATE NOT NULL, 31 | tradeno INTEGER NOT NULL, 32 | price DECIMAL(10,2), 33 | quantity INTEGER, 34 | PRIMARY KEY(tradedate, tradeno) 35 | ); 36 | 37 | DROP TABLE IF EXISTS pkless; 38 | CREATE TABLE pkless ( 39 | foo VARCHAR(20), 40 | bar VARCHAR(20), 41 | baz VARCHAR(20) 42 | ); 43 | -------------------------------------------------------------------------------- /tests/unit/Config/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | getConfigTreeBuilder(); 17 | 18 | $expected = 'Symfony\Component\Config\Definition\Builder\TreeBuilder'; 19 | $this->assertInstanceOf($expected, $builder); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/unit/Config/LoaderTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 17 | 18 | $loader = new ArrayLoader(); 19 | 20 | $this->assertSame($config, $loader->load($config)); 21 | 22 | $this->assertTrue($loader->supports([])); 23 | $this->assertFalse($loader->supports("")); 24 | $this->assertFalse($loader->supports(123)); 25 | $this->assertFalse($loader->supports(new \stdClass)); 26 | } 27 | 28 | public function testJsonLoader() 29 | { 30 | $config = ['foo' => 'bar']; 31 | $json = json_encode($config); 32 | 33 | $tempFile = tempnam(sys_get_temp_dir(), "pho") . ".json"; 34 | file_put_contents($tempFile, $json); 35 | 36 | $loader = new JsonLoader(); 37 | 38 | $this->assertSame($config, $loader->load($tempFile)); 39 | 40 | $this->assertTrue($loader->supports("foo.json")); 41 | $this->assertFalse($loader->supports("foo.yaml")); 42 | $this->assertFalse($loader->supports(123)); 43 | $this->assertFalse($loader->supports([])); 44 | $this->assertFalse($loader->supports(new \stdClass)); 45 | 46 | unlink($tempFile); 47 | } 48 | 49 | /** 50 | * @expectedException Phormium\Exception\ConfigurationException 51 | * @expectedExceptionMessage Failed parsing JSON configuration file. 52 | */ 53 | public function testJsonLoaderInvalidSyntax() 54 | { 55 | $tempFile = tempnam(sys_get_temp_dir(), "pho") . ".json"; 56 | file_put_contents($tempFile, "this is not json"); 57 | 58 | $loader = new JsonLoader(); 59 | $loader->load($tempFile); 60 | 61 | unlink($tempFile); 62 | } 63 | 64 | public function testYamlLoader() 65 | { 66 | $config = ['foo' => 'bar']; 67 | $yaml = Yaml::dump($config); 68 | 69 | $tempFile = tempnam(sys_get_temp_dir(), "pho") . ".yaml"; 70 | file_put_contents($tempFile, $yaml); 71 | 72 | $loader = new YamlLoader(); 73 | 74 | $this->assertSame($config, $loader->load($tempFile)); 75 | 76 | $this->assertTrue($loader->supports("foo.yaml")); 77 | $this->assertFalse($loader->supports("foo.json")); 78 | $this->assertFalse($loader->supports(123)); 79 | $this->assertFalse($loader->supports([])); 80 | $this->assertFalse($loader->supports(new \stdClass)); 81 | 82 | unlink($tempFile); 83 | } 84 | 85 | /** 86 | * @expectedException Phormium\Exception\ConfigurationException 87 | * @expectedExceptionMessage Config file not found at "doesnotexist.yaml". 88 | */ 89 | public function testLoadFileFailed() 90 | { 91 | $loader = new YamlLoader(); 92 | $loader->load("doesnotexist.yaml"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/unit/Config/PostProcessorTest.php: -------------------------------------------------------------------------------- 1 | assertSame(PDO::ATTR_SERVER_INFO, $processor->processConstant(PDO::ATTR_SERVER_INFO)); 20 | $this->assertSame(PDO::PARAM_BOOL, $processor->processConstant(PDO::PARAM_BOOL)); 21 | $this->assertSame(PDO::FETCH_LAZY, $processor->processConstant(PDO::FETCH_LAZY)); 22 | 23 | // Strings mapped to constant values 24 | $this->assertSame(PDO::ATTR_SERVER_INFO, $processor->processConstant("PDO::ATTR_SERVER_INFO")); 25 | $this->assertSame(PDO::PARAM_BOOL, $processor->processConstant("PDO::PARAM_BOOL")); 26 | $this->assertSame(PDO::FETCH_LAZY, $processor->processConstant("PDO::FETCH_LAZY")); 27 | 28 | // Allowed scalars 29 | $this->assertSame("foo", $processor->processConstant("foo", true)); 30 | $this->assertSame(123, $processor->processConstant(123, true)); 31 | } 32 | 33 | /** 34 | * @expectedException Phormium\Exception\ConfigurationException 35 | * @expectedExceptionMessage Invalid constant value 36 | */ 37 | public function testProcessConstantError1() 38 | { 39 | $processor = new PostProcessor(); 40 | $processor->processConstant([]); 41 | } 42 | 43 | /** 44 | * @expectedException Phormium\Exception\ConfigurationException 45 | * @expectedExceptionMessage Invalid constant value 46 | */ 47 | public function testProcessConstantError2() 48 | { 49 | $processor = new PostProcessor(); 50 | $processor->processConstant("foo", false); 51 | } 52 | 53 | public function testProcessConfig() 54 | { 55 | $config = [ 56 | "databases" => [ 57 | "one" => [ 58 | "dsn" => "mysql:host=localhost", 59 | "attributes" => [ 60 | "PDO::ATTR_CASE" => "PDO::CASE_LOWER", 61 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 62 | "PDO::ATTR_STRINGIFY_FETCHES" => false, 63 | PDO::ATTR_TIMEOUT => 10 64 | ] 65 | ] 66 | ] 67 | ]; 68 | 69 | $expected = [ 70 | "databases" => [ 71 | "one" => [ 72 | "dsn" => "mysql:host=localhost", 73 | "username" => null, 74 | "password" => null, 75 | "driver" => "mysql", 76 | "attributes" => [ 77 | PDO::ATTR_CASE => PDO::CASE_LOWER, 78 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 79 | PDO::ATTR_STRINGIFY_FETCHES => false, 80 | PDO::ATTR_TIMEOUT => 10 81 | ] 82 | ] 83 | ] 84 | ]; 85 | 86 | $processor = new PostProcessor(); 87 | $actual = $processor->processConfig($config); 88 | $this->assertEquals($expected, $actual); 89 | } 90 | 91 | /** 92 | * @expectedException Phormium\Exception\ConfigurationException 93 | * @expectedExceptionMessage Invalid attribute "foo" specified in configuration for database "one". 94 | */ 95 | public function testProcessConfigError1() 96 | { 97 | $processor = new PostProcessor(); 98 | $processor->processConfig([ 99 | "databases" => [ 100 | "one" => [ 101 | "dsn" => "mysql:host=localhost", 102 | "attributes" => [ 103 | "foo" => 10 104 | ] 105 | ] 106 | ] 107 | ]); 108 | } 109 | 110 | /** 111 | * @expectedException Phormium\Exception\ConfigurationException 112 | * @expectedExceptionMessage Invalid value given for attribute "PDO::ATTR_TIMEOUT", in configuration for database "one". 113 | */ 114 | public function testProcessConfigError2() 115 | { 116 | $processor = new PostProcessor(); 117 | $processor->processConfig([ 118 | "databases" => [ 119 | "one" => [ 120 | "dsn" => "mysql:host=localhost", 121 | "attributes" => [ 122 | "PDO::ATTR_TIMEOUT" => [] 123 | ] 124 | ] 125 | ] 126 | ]); 127 | } 128 | 129 | public function testParseDriver() 130 | { 131 | $proc = new PostProcessor(); 132 | 133 | $this->assertSame('informix', $proc->parseDriver('informix:host=localhost')); 134 | $this->assertSame('mysql', $proc->parseDriver('mysql:host=localhost')); 135 | $this->assertSame('pgsql', $proc->parseDriver('pgsql:host=localhost')); 136 | $this->assertSame('sqlite', $proc->parseDriver('sqlite:host=localhost')); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/unit/Database/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getAttribute') 29 | ->with(PDO::ATTR_DRIVER_NAME) 30 | ->once() 31 | ->andReturn($driver); 32 | 33 | $conn = new Connection($pdo, $emitter); 34 | 35 | $this->assertSame($driver, $conn->getDriver()); 36 | $this->assertSame($emitter, $conn->getEmitter()); 37 | $this->assertSame($pdo, $conn->getPDO()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/unit/Database/FactoryTest.php: -------------------------------------------------------------------------------- 1 | [ 19 | "dsn" => "sqlite:tmp/db1.db", 20 | "driver" => "sqlite", 21 | "username" => null, 22 | "password" => null, 23 | "attributes" => [] 24 | ], 25 | "db2" => [ 26 | "dsn" => "sqlite:tmp/db2.db", 27 | "driver" => "sqlite", 28 | "username" => null, 29 | "password" => null, 30 | "attributes" => [] 31 | ] 32 | ]; 33 | 34 | public function tearDown() 35 | { 36 | m::close(); 37 | } 38 | 39 | protected function getMockEmitter() 40 | { 41 | return m::mock("Evenement\\EventEmitter"); 42 | } 43 | 44 | public function testAttributes1() 45 | { 46 | $emitter = $this->getMockEmitter(); 47 | 48 | $config = $this->config; 49 | $config['db1']['attributes'] = [ 50 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC 51 | ]; 52 | 53 | $factory = new Factory($config, $emitter); 54 | $conn = $factory->newConnection('db1'); 55 | $pdo = $conn->getPDO(); 56 | 57 | $expected = PDO::FETCH_ASSOC; 58 | $actual = $pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE); 59 | $this->assertSame($expected, $actual); 60 | } 61 | 62 | public function testAttributes2() 63 | { 64 | $emitter = $this->getMockEmitter(); 65 | 66 | $config = $this->config; 67 | $config['db1']['attributes'] = [ 68 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_BOTH 69 | ]; 70 | 71 | $factory = new Factory($config, $emitter); 72 | $conn = $factory->newConnection('db1'); 73 | $pdo = $conn->getPDO(); 74 | 75 | $expected = PDO::FETCH_BOTH; 76 | $actual = $pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE); 77 | $this->assertSame($expected, $actual); 78 | } 79 | 80 | public function testAttributesCannotChange() 81 | { 82 | $emitter = $this->getMockEmitter(); 83 | 84 | $config = $this->config; 85 | $config['db1']['attributes'] = [ 86 | PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT 87 | ]; 88 | 89 | $factory = new Factory($config, $emitter); 90 | 91 | // Suppress the warning which breaks the test 92 | $conn = @$factory->newConnection("db1"); 93 | $pdo = $conn->getPDO(); 94 | 95 | // Error mode should be exception, even though it is set to a different 96 | // value in the settings 97 | $expected = PDO::ERRMODE_EXCEPTION; 98 | $actual = $pdo->getAttribute(PDO::ATTR_ERRMODE); 99 | $this->assertSame($expected, $actual); 100 | } 101 | 102 | /** 103 | * @expectedException PHPUnit_Framework_Error_Warning 104 | * @expectedExceptionMessage Attribute PDO::ATTR_ERRMODE is set to something other than PDO::ERRMODE_EXCEPTION for database "db1". This is not allowed because Phormium depends on this setting. Skipping attribute definition. 105 | */ 106 | public function testAttributesCannotChangeError() 107 | { 108 | $emitter = $this->getMockEmitter(); 109 | 110 | $config = $this->config; 111 | $config['db1']['attributes'] = [ 112 | PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT 113 | ]; 114 | 115 | $factory = new Factory($config, $emitter); 116 | $factory->newConnection("db1"); 117 | } 118 | 119 | /** 120 | * @expectedException Exception 121 | * @expectedExceptionMessage Failed setting PDO attribute "foo" to "bar" on database "db1". 122 | */ 123 | public function testInvalidAttribute() 124 | { 125 | $emitter = $this->getMockEmitter(); 126 | 127 | $config = $this->config; 128 | $config['db1']['attributes'] = ["foo" => "bar"]; 129 | 130 | $factory = new Factory($config, $emitter); 131 | @$factory->newConnection("db1"); 132 | } 133 | 134 | /** 135 | * @expectedException Exception 136 | * @expectedExceptionMessage Database "db3" is not configured. 137 | */ 138 | public function testNotConfiguredException() 139 | { 140 | $emitter = $this->getMockEmitter(); 141 | $config = $this->config; 142 | 143 | $factory = new Factory($config, $emitter); 144 | $factory->newConnection("db3"); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/unit/Filter/CompositeFilterTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(CompositeFilter::class, $filter); 26 | $this->assertSame(CompositeFilter::OP_AND, $filter->operation()); 27 | $this->assertSame($subfilters, $filter->filters()); 28 | 29 | $filter = Filter::_or(...$subfilters); 30 | $this->assertInstanceOf(CompositeFilter::class, $filter); 31 | $this->assertSame(CompositeFilter::OP_OR, $filter->operation()); 32 | $this->assertSame($subfilters, $filter->filters()); 33 | } 34 | 35 | public function testArrayToFilter() 36 | { 37 | $filter = new CompositeFilter(CompositeFilter::OP_AND, [ 38 | ["foo", "=", "bar"], 39 | ["bla", "not null"], 40 | ]); 41 | 42 | $filters = $filter->filters(); 43 | 44 | $this->assertCount(2, $filters); 45 | 46 | $this->assertInstanceOf(ColumnFilter::class, $filters[0]); 47 | $this->assertSame("=", $filters[0]->operation()); 48 | $this->assertSame("foo", $filters[0]->column()); 49 | $this->assertSame("bar", $filters[0]->value()); 50 | 51 | $this->assertInstanceOf(ColumnFilter::class, $filters[1]); 52 | $this->assertSame("NOT NULL", $filters[1]->operation()); 53 | $this->assertSame("bla", $filters[1]->column()); 54 | $this->assertNull($filters[1]->value()); 55 | } 56 | 57 | /** 58 | * @expectedException Phormium\Exception\InvalidQueryException 59 | * @expectedExceptionMessage Invalid composite filter operation [foo]. Expected one of: AND, OR 60 | */ 61 | public function testInvalidOperation() 62 | { 63 | new CompositeFilter('foo'); 64 | } 65 | 66 | /** 67 | * @expectedException Phormium\Exception\InvalidQueryException 68 | * @expectedExceptionMessage CompositeFilter requires an array of Filter objects as second argument, got [string]. 69 | */ 70 | public function testInvalidSubfilter() 71 | { 72 | new CompositeFilter(CompositeFilter::OP_AND, ["foo"]); 73 | } 74 | 75 | public function testWithAdded() 76 | { 77 | $sf1 = m::mock(Filter::class); 78 | $sf2 = m::mock(Filter::class); 79 | $sf3 = m::mock(Filter::class); 80 | 81 | $f1 = new CompositeFilter(CompositeFilter::OP_AND); 82 | $f2 = $f1->withAdded($sf1); 83 | $f3 = $f2->withAdded($sf2); 84 | $f4 = $f3->withAdded($sf3); 85 | 86 | $this->assertNotSame($f1, $f2); 87 | $this->assertNotSame($f2, $f3); 88 | $this->assertNotSame($f3, $f4); 89 | 90 | $this->assertSame([], $f1->filters()); 91 | $this->assertSame([$sf1], $f2->filters()); 92 | $this->assertSame([$sf1, $sf2], $f3->filters()); 93 | $this->assertSame([$sf1, $sf2, $sf3], $f4->filters()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/unit/Filter/FilterTest.php: -------------------------------------------------------------------------------- 1 | assertSame("foo", $col->column()); 23 | $this->assertSame("=", $col->operation()); 24 | $this->assertSame(1, $col->value()); 25 | 26 | $this->assertSame("lower(a) = ?", $raw->condition()); 27 | $this->assertSame([2], $raw->arguments()); 28 | 29 | $this->assertSame(CompositeFilter::OP_AND, $and->operation()); 30 | $this->assertSame([$col, $raw], $and->filters()); 31 | 32 | $this->assertSame(CompositeFilter::OP_OR, $or->operation()); 33 | $this->assertSame([$raw, $col], $or->filters()); 34 | } 35 | 36 | public function testFactory() 37 | { 38 | $col = Filter::col("foo", "=", 1); 39 | $f = Filter::factory($col); 40 | $this->assertSame($f, $col); 41 | 42 | // Column filter in array, 3 args 43 | $f = Filter::factory(['foo', '=', 1]); 44 | $this->assertInstanceOf(ColumnFilter::class, $f); 45 | $this->assertSame('foo', $f->column()); 46 | $this->assertSame(ColumnFilter::OP_EQUALS, $f->operation()); 47 | $this->assertSame(1, $f->value()); 48 | 49 | // Column filter no array, 3 args 50 | $f = Filter::factory('foo', '=', 1); 51 | $this->assertInstanceOf(ColumnFilter::class, $f); 52 | $this->assertSame('foo', $f->column()); 53 | $this->assertSame(ColumnFilter::OP_EQUALS, $f->operation()); 54 | $this->assertSame(1, $f->value()); 55 | 56 | // Column filter in array, 2 args 57 | $f = Filter::factory(['foo', 'is null']); 58 | $this->assertInstanceOf(ColumnFilter::class, $f); 59 | $this->assertSame('foo', $f->column()); 60 | $this->assertSame(ColumnFilter::OP_IS_NULL, $f->operation()); 61 | $this->assertNull($f->value()); 62 | 63 | // Column filter no array, 2 args 64 | $f = Filter::factory('foo', 'is null'); 65 | $this->assertInstanceOf(ColumnFilter::class, $f); 66 | $this->assertSame('foo', $f->column()); 67 | $this->assertSame(ColumnFilter::OP_IS_NULL, $f->operation()); 68 | $this->assertNull($f->value()); 69 | 70 | // Raw filter, no arguments 71 | $f = Filter::factory('bla(tra)'); 72 | $this->assertInstanceOf(RawFilter::class, $f); 73 | $this->assertSame('bla(tra)', $f->condition()); 74 | $this->assertSame([], $f->arguments()); 75 | 76 | // Raw filter, with arguments 77 | $f = Filter::factory('bla(tra)', [1, 2, 3]); 78 | $this->assertInstanceOf(RawFilter::class, $f); 79 | $this->assertSame('bla(tra)', $f->condition()); 80 | $this->assertSame([1, 2, 3], $f->arguments()); 81 | } 82 | 83 | /** 84 | * @expectedException Phormium\Exception\InvalidQueryException 85 | * @expectedExceptionMessage Invalid filter arguments. 86 | */ 87 | public function testFactoryInvalidInput() 88 | { 89 | Filter::factory(1, 2, 3, 4, 5); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/unit/Filter/RawFilterTest.php: -------------------------------------------------------------------------------- 1 | assertSame($condition, $filter->condition()); 23 | $this->assertSame($arguments, $filter->arguments()); 24 | } 25 | 26 | function testFactory() 27 | { 28 | $condition = "lower(name) = ?"; 29 | $arguments = ['foo']; 30 | 31 | $filter = Filter::raw($condition, $arguments); 32 | 33 | $this->assertSame($condition, $filter->condition()); 34 | $this->assertSame($arguments, $filter->arguments()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/Helper/AssertTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Assert::isInteger(10)); 16 | $this->assertTrue(Assert::isInteger(0)); 17 | $this->assertTrue(Assert::isInteger(-10)); 18 | $this->assertTrue(Assert::isInteger("10")); 19 | $this->assertTrue(Assert::isInteger("0")); 20 | $this->assertTrue(Assert::isInteger("-10")); 21 | 22 | $this->assertFalse(Assert::isInteger(10.6)); 23 | $this->assertFalse(Assert::isInteger("10.6")); 24 | $this->assertFalse(Assert::isInteger("heavy metal")); 25 | $this->assertFalse(Assert::isInteger([])); 26 | $this->assertFalse(Assert::isInteger(new \stdClass())); 27 | $this->assertFalse(Assert::isInteger("")); 28 | $this->assertFalse(Assert::isInteger("-")); 29 | } 30 | 31 | public function testIsPositiveInteger() 32 | { 33 | $this->assertTrue(Assert::isPositiveInteger(10)); 34 | $this->assertTrue(Assert::isPositiveInteger(0)); 35 | $this->assertTrue(Assert::isPositiveInteger("10")); 36 | $this->assertTrue(Assert::isPositiveInteger("0")); 37 | 38 | $this->assertFalse(Assert::isPositiveInteger(10.6)); 39 | $this->assertFalse(Assert::isPositiveInteger("10.6")); 40 | $this->assertFalse(Assert::isPositiveInteger("heavy metal")); 41 | $this->assertFalse(Assert::isPositiveInteger([])); 42 | $this->assertFalse(Assert::isPositiveInteger(new \stdClass())); 43 | $this->assertFalse(Assert::isPositiveInteger("")); 44 | $this->assertFalse(Assert::isPositiveInteger("-")); 45 | $this->assertFalse(Assert::isPositiveInteger(-10)); 46 | $this->assertFalse(Assert::isPositiveInteger("-10")); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/Query/AggregateTest.php: -------------------------------------------------------------------------------- 1 | assertSame("avg", $agg->type()); 15 | $this->assertSame("foo", $agg->column()); 16 | 17 | $agg = new Aggregate(Aggregate::COUNT); 18 | $this->assertSame("count", $agg->type()); 19 | $this->assertSame("*", $agg->column()); 20 | } 21 | 22 | /** 23 | * @expectedException Phormium\Exception\InvalidQueryException 24 | * @expectedExceptionMessage Invalid aggregate type [xxx]. 25 | */ 26 | public function testInvalidType() 27 | { 28 | $agg = new Aggregate('xxx', 'yyy'); 29 | } 30 | 31 | /** 32 | * @expectedException Phormium\Exception\InvalidQueryException 33 | * @expectedExceptionMessage Aggregate type [avg] requires a column to be given. 34 | */ 35 | public function testRequiresColumnError() 36 | { 37 | $agg = new Aggregate(Aggregate::AVERAGE); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/unit/Query/ColumnOrderTest.php: -------------------------------------------------------------------------------- 1 | assertSame("foo", $order->column()); 16 | $this->assertSame("asc", $order->direction()); 17 | 18 | $order = new ColumnOrder("bar", "desc"); 19 | 20 | $this->assertSame("bar", $order->column()); 21 | $this->assertSame("desc", $order->direction()); 22 | } 23 | 24 | public function testFactories() 25 | { 26 | $order = ColumnOrder::asc("foo"); 27 | 28 | $this->assertSame("foo", $order->column()); 29 | $this->assertSame("asc", $order->direction()); 30 | 31 | $order = ColumnOrder::desc("bar"); 32 | 33 | $this->assertSame("bar", $order->column()); 34 | $this->assertSame("desc", $order->direction()); 35 | } 36 | 37 | /** 38 | * @expectedException Phormium\Exception\OrmException 39 | * @expectedExceptionMessage Invalid $direction [bar]. Expected one of [asc, desc] 40 | */ 41 | public function testInvalidDirection() 42 | { 43 | new ColumnOrder("foo", "bar"); 44 | } 45 | 46 | /** 47 | * @expectedException Phormium\Exception\OrmException 48 | * @expectedExceptionMessage Invalid $column type [array], expected string. 49 | */ 50 | public function testInvalidColumn() 51 | { 52 | new ColumnOrder([], "asc"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit/Query/LimitOffsetTest.php: -------------------------------------------------------------------------------- 1 | assertSame(10, $lo->limit()); 15 | $this->assertSame(20, $lo->offset()); 16 | 17 | $lo = new LimitOffset(10); 18 | $this->assertSame(10, $lo->limit()); 19 | $this->assertNull($lo->offset()); 20 | } 21 | 22 | /** 23 | * @expectedException Phormium\Exception\OrmException 24 | * @expectedExceptionMessage $limit must be a positive integer or null. 25 | */ 26 | public function testInvalidLimit1() 27 | { 28 | new LimitOffset(-1); 29 | } 30 | 31 | /** 32 | * @expectedException Phormium\Exception\OrmException 33 | * @expectedExceptionMessage $limit must be a positive integer or null. 34 | */ 35 | public function testInvalidLimit2() 36 | { 37 | new LimitOffset('foo'); 38 | } 39 | 40 | /** 41 | * @expectedException Phormium\Exception\OrmException 42 | * @expectedExceptionMessage $offset must be a positive integer or null. 43 | */ 44 | public function testInvalidOffset() 45 | { 46 | new LimitOffset(1, -1); 47 | } 48 | 49 | /** 50 | * @expectedException Phormium\Exception\OrmException 51 | * @expectedExceptionMessage $offset cannot be given without a $limit 52 | */ 53 | public function testOffsetWithoutLimit() 54 | { 55 | new LimitOffset(null, 1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/Query/OrderByTest.php: -------------------------------------------------------------------------------- 1 | assertCount(2, $orderBy->orders()); 19 | $this->assertSame($co1, $orderBy->orders()[0]); 20 | $this->assertSame($co2, $orderBy->orders()[1]); 21 | } 22 | 23 | public function testAdding() 24 | { 25 | $co1 = new ColumnOrder("foo", ColumnOrder::ASCENDING); 26 | $co2 = new ColumnOrder("foo", ColumnOrder::ASCENDING); 27 | 28 | $ob1 = new OrderBy([$co1]); 29 | $ob2 = $ob1->withAdded($co2); 30 | 31 | $this->assertNotSame($ob1, $ob2); 32 | 33 | $this->assertCount(1, $ob1->orders()); 34 | $this->assertSame($co1, $ob1->orders()[0]); 35 | 36 | $this->assertCount(2, $ob2->orders()); 37 | $this->assertSame($co1, $ob2->orders()[0]); 38 | $this->assertSame($co2, $ob2->orders()[1]); 39 | } 40 | 41 | /** 42 | * @expectedException Phormium\Exception\OrmException 43 | * @expectedExceptionMessage OrderBy needs at least one ColumnOrder element, empty array given. 44 | */ 45 | public function testEmptyOrder() 46 | { 47 | $orderBy = new OrderBy([]); 48 | } 49 | 50 | /** 51 | * @expectedException Phormium\Exception\OrmException 52 | * @expectedExceptionMessage Expected $orders to be instances of Phormium\Query\ColumnOrder. Given [string]. 53 | */ 54 | public function testInvalidOrder() 55 | { 56 | $orderBy = new OrderBy(["foo"]); 57 | } 58 | 59 | // /** 60 | // * @expectedException Phormium\Exception\OrmException 61 | // * @expectedExceptionMessage $limit must be a positive integer or null. 62 | // */ 63 | // public function testInvalidLimit2() 64 | // { 65 | // new LimitOffset('foo'); 66 | // } 67 | 68 | // /** 69 | // * @expectedException Phormium\Exception\OrmException 70 | // * @expectedExceptionMessage $offset must be a positive integer or null. 71 | // */ 72 | // public function testInvalidOffset() 73 | // { 74 | // new LimitOffset(1, -1); 75 | // } 76 | 77 | // /** 78 | // * @expectedException Phormium\Exception\OrmException 79 | // * @expectedExceptionMessage $offset cannot be given without a $limit 80 | // */ 81 | // public function testOffsetWithoutLimit() 82 | // { 83 | // new LimitOffset(null, 1); 84 | // } 85 | } 86 | -------------------------------------------------------------------------------- /tests/unit/Query/QuerySegmentTest.php: -------------------------------------------------------------------------------- 1 | assertSame($query, $qs->query()); 18 | $this->assertSame($args, $qs->args()); 19 | 20 | // Default args 21 | $qs = new QuerySegment(); 22 | $this->assertSame("", $qs->query()); 23 | $this->assertSame([], $qs->args()); 24 | } 25 | 26 | public function testCombine() 27 | { 28 | $qs1 = new QuerySegment("WHERE a = ?", ["foo"]); 29 | $qs2 = new QuerySegment("AND b = ?", ["bar"]); 30 | 31 | $qsc = $qs1->combine($qs2); 32 | $this->assertSame("WHERE a = ? AND b = ?", $qsc->query()); 33 | $this->assertSame(["foo", "bar"], $qsc->args()); 34 | } 35 | 36 | public function testReduce() 37 | { 38 | $qs1 = new QuerySegment("SELECT *", []); 39 | $qs2 = new QuerySegment("FROM table", []); 40 | $qs3 = new QuerySegment("WHERE a = ?", ["foo"]); 41 | $qs4 = new QuerySegment("AND b BETWEEN ? AND ?", ["bar", "baz"]); 42 | $qs5 = new QuerySegment("AND c > ?", ["qux"]); 43 | 44 | $reduced = QuerySegment::reduce([$qs1, $qs2, $qs3, $qs4, $qs5]); 45 | 46 | $expectedQuery = "SELECT * FROM table WHERE a = ? AND b BETWEEN ? AND ? AND c > ?"; 47 | $expectedArgs = ["foo", "bar", "baz", "qux"]; 48 | 49 | $this->assertInstanceOf("Phormium\Query\QuerySegment", $reduced); 50 | $this->assertSame($expectedQuery, $reduced->query()); 51 | $this->assertSame($expectedArgs, $reduced->args()); 52 | } 53 | 54 | public function testImplode() 55 | { 56 | $qs1 = new QuerySegment("foo", [1]); 57 | $qs2 = new QuerySegment("bar", []); 58 | $qs3 = new QuerySegment("baz", [3, 4]); 59 | $separator = new QuerySegment("x", ['y']); 60 | 61 | $imploded = QuerySegment::implode($separator, [$qs1, $qs2, $qs3]); 62 | 63 | $expectedQuery = "foo x bar x baz"; 64 | $expectedArgs = [1, 'y', 'y', 3, 4]; 65 | 66 | $this->assertInstanceOf("Phormium\Query\QuerySegment", $imploded); 67 | $this->assertSame($expectedQuery, $imploded->query()); 68 | $this->assertSame($expectedArgs, $imploded->args()); 69 | } 70 | 71 | public function testImplodeEmpty() 72 | { 73 | $separator = new QuerySegment("x", ['y']); 74 | 75 | $imploded = QuerySegment::implode($separator, []); 76 | $this->assertSame("", $imploded->query()); 77 | $this->assertSame([], $imploded->args()); 78 | 79 | } 80 | 81 | public function testImplodeSingle() 82 | { 83 | $segment = new QuerySegment("foo", ['bar']); 84 | $separator = new QuerySegment("bla", ['tra']); 85 | 86 | $imploded = QuerySegment::implode($separator, [$segment]); 87 | $this->assertSame($segment, $imploded); 88 | 89 | } 90 | 91 | public function testEmbrace() 92 | { 93 | $query = "a = ? AND b = ?"; 94 | $args = [1, 2]; 95 | 96 | $segment = new QuerySegment($query, $args); 97 | $embraced = QuerySegment::embrace($segment); 98 | 99 | $this->assertSame("($query)", $embraced->query()); 100 | $this->assertSame($args, $embraced->args()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/unit/QueryBuilder/CompositeFilterRendererTest.php: -------------------------------------------------------------------------------- 1 | renderFilter($filter); 22 | } 23 | 24 | public function testCompositeFilter1() 25 | { 26 | $filter = new CompositeFilter( 27 | CompositeFilter::OP_OR, 28 | [ 29 | ColumnFilter::fromArray(['id', '=', 1]), 30 | ColumnFilter::fromArray(['id', '=', 2]), 31 | ColumnFilter::fromArray(['id', '=', 3]), 32 | ] 33 | ); 34 | 35 | $actual = $this->render($filter); 36 | $expected = new QuerySegment('("id" = ? OR "id" = ? OR "id" = ?)', [1, 2, 3]); 37 | $this->assertEquals($expected, $actual); 38 | } 39 | 40 | public function testCompositeFilter2() 41 | { 42 | $filter = new CompositeFilter( 43 | CompositeFilter::OP_OR, 44 | [ 45 | ['id', '=', 1], 46 | ['id', '=', 2], 47 | ['id', '=', 3], 48 | ] 49 | ); 50 | 51 | $actual = $this->render($filter); 52 | $expected = new QuerySegment('("id" = ? OR "id" = ? OR "id" = ?)', [1, 2, 3]); 53 | $this->assertEquals($expected, $actual); 54 | } 55 | 56 | /** 57 | * @expectedException \Exception 58 | * @expectedExceptionMessage Canot render composite filter. No filters defined. 59 | */ 60 | public function testRenderEmpty() 61 | { 62 | $filter = new CompositeFilter("AND"); 63 | $this->render($filter); 64 | } 65 | } -------------------------------------------------------------------------------- /tests/unit/QueryBuilder/RawFilterRendererTest.php: -------------------------------------------------------------------------------- 1 | renderFilter($filter); 21 | } 22 | 23 | function testConstruction() 24 | { 25 | $condition = "lower(name) = ?"; 26 | $arguments = ['foo']; 27 | 28 | $filter = new RawFilter($condition, $arguments); 29 | $actual = $this->render($filter); 30 | $expected = new QuerySegment($condition, $arguments); 31 | 32 | $this->assertEquals($expected, $actual); 33 | } 34 | } --------------------------------------------------------------------------------