├── .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 | [](https://packagist.org/packages/phormium/phormium) [](https://packagist.org/packages/phormium/phormium) [](https://travis-ci.org/ihabunek/phormium) [](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 | [](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 | }
--------------------------------------------------------------------------------