├── .codeclimate.yml
├── .github
└── workflows
│ └── push.yml
├── .gitignore
├── CONTRIBUTE.md
├── README.md
├── composer.json
├── docs
├── _config.yml
├── _data
│ └── menu.yml
├── _layouts
│ └── default.html
├── _reference
│ ├── class.md.twig
│ ├── index.md.twig
│ ├── method.md.twig
│ └── template.xml
├── bulkInserts.md
├── configuration.md
├── entities.md
├── entityDefinition.md
├── events.md
├── filters.md
├── index.md
├── javascripts
│ └── scale.fix.js
├── querybuilder.md
├── reference.md
├── relationDefinition.md
├── relations.md
├── stylesheets
│ ├── highlight.css
│ └── styles.css
├── testing.md
└── validate.md
├── examples
├── bootstrap.php
├── entities.php
├── fetch.php
├── filter.php
├── morphed.php
└── observe.php
├── icon.svg
├── logo.svg
├── phpdoc.xml
├── phpunit.xml
├── src
├── BulkInsert.php
├── DbConfig.php
├── Dbal
│ ├── Column.php
│ ├── Dbal.php
│ ├── Error.php
│ ├── Error
│ │ ├── InvalidJson.php
│ │ ├── NoBoolean.php
│ │ ├── NoDateTime.php
│ │ ├── NoNumber.php
│ │ ├── NoString.php
│ │ ├── NoTime.php
│ │ ├── NotAllowed.php
│ │ ├── NotNullable.php
│ │ ├── NotValid.php
│ │ └── TooLong.php
│ ├── Escaping.php
│ ├── Expression.php
│ ├── Mysql.php
│ ├── Other.php
│ ├── Pgsql.php
│ ├── QueryLanguage
│ │ ├── CompositeInExpression.php
│ │ ├── CompositeInValuesExpression.php
│ │ ├── DeleteStatement.php
│ │ ├── InsertStatement.php
│ │ ├── UpdateFromStatement.php
│ │ ├── UpdateJoinStatement.php
│ │ ├── UpdateStatement.php
│ │ └── WhereClause.php
│ ├── Sqlite.php
│ ├── Table.php
│ ├── Type.php
│ ├── Type
│ │ ├── Boolean.php
│ │ ├── DateTime.php
│ │ ├── Enum.php
│ │ ├── Json.php
│ │ ├── Number.php
│ │ ├── Set.php
│ │ ├── Text.php
│ │ ├── Time.php
│ │ └── VarChar.php
│ └── TypeInterface.php
├── EM.php
├── Entity.php
├── Entity
│ ├── Booting.php
│ ├── EventHandlers.php
│ ├── GeneratesPrimaryKeys.php
│ ├── Naming.php
│ ├── Relations.php
│ └── Validation.php
├── EntityFetcher.php
├── EntityFetcher
│ ├── AppliesFilters.php
│ ├── CallableFilter.php
│ ├── ExecutesQueries.php
│ ├── FilterInterface.php
│ ├── MakesJoins.php
│ └── TranslatesClasses.php
├── EntityManager.php
├── Event.php
├── Event
│ ├── Changed.php
│ ├── Deleted.php
│ ├── Deleting.php
│ ├── Fetched.php
│ ├── Inserted.php
│ ├── Inserting.php
│ ├── Saved.php
│ ├── Saving.php
│ ├── UpdateEvent.php
│ ├── Updated.php
│ └── Updating.php
├── Exception.php
├── Exception
│ ├── IncompletePrimaryKey.php
│ ├── InvalidArgument.php
│ ├── InvalidConfiguration.php
│ ├── InvalidName.php
│ ├── InvalidRelation.php
│ ├── InvalidType.php
│ ├── NoConnection.php
│ ├── NoEntity.php
│ ├── NoEntityManager.php
│ ├── NoOperator.php
│ ├── NotJoined.php
│ ├── NotScalar.php
│ ├── UndefinedRelation.php
│ ├── UnknownColumn.php
│ └── UnsupportedDriver.php
├── Helper.php
├── MockTrait.php
├── Namer.php
├── Observer
│ ├── AbstractObserver.php
│ └── CallbackObserver.php
├── ObserverInterface.php
├── QueryBuilder
│ ├── ExecutesQueries.php
│ ├── HasWhereConditions.php
│ ├── MakesJoins.php
│ ├── Parenthesis.php
│ ├── ParenthesisInterface.php
│ ├── QueryBuilder.php
│ └── QueryBuilderInterface.php
├── Relation.php
├── Relation
│ ├── HasOpponent.php
│ ├── HasReference.php
│ ├── ManyToMany.php
│ ├── Morphed.php
│ ├── OneToMany.php
│ ├── OneToOne.php
│ ├── Owner.php
│ └── ParentChildren.php
└── Testing
│ ├── EntityFetcherMock.php
│ ├── EntityFetcherMock
│ ├── Result.php
│ └── ResultRepository.php
│ ├── EntityManagerMock.php
│ └── MocksEntityManager.php
└── tests
├── BulkInsertTest.php
├── ClosureWrapper.php
├── Constraint
└── ArraySubset.php
├── DbConfigTest.php
├── Dbal
├── BasicTest.php
├── BulkInsertTest.php
├── ColumnTest.php
├── DataModificationTest.php
├── EscapeValueTest.php
├── Mysql
│ ├── DescribeTest.php
│ └── UpdateJoinTest.php
├── Other
│ └── UpdateTest.php
├── Pgsql
│ ├── DescribeTest.php
│ └── UpdateFromTest.php
├── Sqlite
│ ├── DescribeTest.php
│ ├── InsertTest.php
│ └── UpdateFromTest.php
├── TransactionTest.php
├── Type
│ ├── BooleanTest.php
│ ├── Custom
│ │ ├── CustomColumn.php
│ │ ├── Point.php
│ │ └── PointTest.php
│ ├── DateTimeTest.php
│ ├── EnumTest.php
│ ├── JsonTest.php
│ ├── NumberTest.php
│ ├── SetTest.php
│ ├── TextTest.php
│ ├── TimeTest.php
│ └── VarCharTest.php
└── ValidateTest.php
├── Entity
├── BasicTest.php
├── BootTestEntity.php
├── BootTestTrait.php
├── BootingTest.php
├── ColumnNameTest.php
├── DataTest.php
├── Examples
│ ├── Article.php
│ ├── Category.php
│ ├── Concerns
│ │ ├── WithCreated.php
│ │ ├── WithTimestamps.php
│ │ └── WithUpdated.php
│ ├── ContactPhone.php
│ ├── DamagedABBRVCase.php
│ ├── GeneratesUuid.php
│ ├── Image.php
│ ├── Psr0_StudlyCaps.php
│ ├── RelationExample.php
│ ├── Snake_Ucfirst.php
│ ├── StaticTableName.php
│ ├── StudlyCaps.php
│ ├── Tag.php
│ ├── Taggable.php
│ ├── User.php
│ └── UserContact.php
├── ExistsTest.php
├── HelperTest.php
├── IssetTest.php
├── RelationsTest.php
├── SaveEntityTest.php
├── TableNameTest.php
├── ToArrayTest.php
└── ValidateTest.php
├── EntityFetcher
├── BasicTest.php
├── CountTest.php
├── EagerLoadTest.php
├── Examples
│ └── NotDeletedFilter.php
├── ExecutesQueriesTest.php
└── FilterTest.php
├── EntityManager
├── BulkInsertTest.php
├── ConnectionsTest.php
├── DataModificationTest.php
├── DescribeTest.php
├── EagerLoadTest.php
├── Examples
│ ├── Concrete.php
│ ├── Entity.php
│ ├── SubNamespace
│ │ └── Entity.php
│ ├── Unspecified.php
│ └── functions.php
├── GetInstanceTest.php
├── MappingTest.php
└── OptionsTest.php
├── Examples
├── AuditObserver.php
├── CustomEvent.php
└── DateTimeDerivate.php
├── ExceptionsTest.php
├── HelperTest.php
├── NamerTest.php
├── Observer
├── AbstractObserverTest.php
├── CallbackObserverTest.php
├── EventTest.php
├── FireEventTest.php
└── ObserverRegistrationTest.php
├── QueryBuilder
├── BasicTest.php
├── FetchTest.php
├── JoinTest.php
└── WhereConditionsTest.php
├── Relation
├── ManyToManyTest.php
├── MorphedRelationTest.php
├── OneToManyTest.php
├── OneToOneTest.php
├── OwnerTest.php
├── ParentChildrenTest.php
└── RelationsFilterTest.php
├── TestCase.php
├── TestEntity.php
├── TestEntityFetcher.php
├── TestEntityManager.php
└── Testing
├── CreateMockedEntityTest.php
├── EntityFetcherMockTest.php
├── EntityFetcherResultTest.php
├── ExpectDeleteTest.php
├── ExpectFetchTest.php
├── ExpectInsertTest.php
├── ExpectUpdateTest.php
└── InitMockTest.php
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | checks:
4 | method-complexity:
5 | config:
6 | threshold: 9
7 |
8 | exclude_patterns:
9 | - "config/"
10 | - "db/"
11 | - "dist/"
12 | - "docs/"
13 | - "features/"
14 | - "**/node_modules/"
15 | - "script/"
16 | - "**/spec/"
17 | - "**/test/"
18 | - "**/tests/"
19 | - "Tests/"
20 | - "example.php"
21 | - "examples/"
22 | - "**/vendor/"
23 | - "**/*_test.go"
24 | - "**/*.d.ts"
25 |
26 | plugins:
27 | phpcodesniffer:
28 | enabled: true
29 | config:
30 | standard: "PSR2"
31 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | jobs:
3 | before:
4 | runs-on: ubuntu-latest
5 | steps:
6 | - name: Setup Code-Climate
7 | uses: amancevice/setup-code-climate@v1
8 | with:
9 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }}
10 |
11 | - name: Prepare CodeClimate
12 | run: cc-test-reporter before-build
13 |
14 | unit-tests:
15 | needs: [before]
16 | strategy:
17 | matrix:
18 | php-version: ["7.3", "7.4", "8.0", "8.1", "8.2"]
19 | name: PHP Unit Tests on PHP ${{ matrix.php-version }}
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 |
25 | - name: Setup PHP
26 | uses: shivammathur/setup-php@v2
27 | with:
28 | php-version: ${{ matrix.php-version }}
29 |
30 | - name: Setup Code-Climate
31 | uses: amancevice/setup-code-climate@v1
32 | with:
33 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }}
34 |
35 | - name: Get composer cache directory
36 | id: composer-cache
37 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
38 |
39 | - name: Cache dependencies
40 | uses: actions/cache@v3
41 | with:
42 | path: ${{ steps.composer-cache.outputs.dir }}
43 | key: composer-cache-${{ matrix.php-version }}
44 |
45 | - name: Install dependencies
46 | run: composer install --no-interaction --ansi
47 |
48 | - name: Execute tests
49 | run: |
50 | php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit \
51 | -c phpunit.xml \
52 | --coverage-clover=coverage/clover.xml \
53 | --coverage-text \
54 | --color=always
55 |
56 | - name: Format Coverage
57 | run: cc-test-reporter format-coverage -t clover -o coverage/cc-${{ matrix.php-version }}.json coverage/clover.xml
58 |
59 | - name: Store Coverage Result
60 | uses: actions/upload-artifact@v3
61 | with:
62 | name: coverage-results
63 | path: coverage/
64 |
65 | after:
66 | needs: [unit-tests]
67 | runs-on: ubuntu-latest
68 | steps:
69 | - name: Restore Coverage Result
70 | uses: actions/download-artifact@v3
71 | with:
72 | name: coverage-results
73 | path: coverage/
74 |
75 | - name: Setup Code-Climate
76 | uses: amancevice/setup-code-climate@v1
77 | with:
78 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }}
79 |
80 | - name: Report Coverage
81 | run: |
82 | cc-test-reporter sum-coverage coverage/cc-*.json -p 5 -o coverage/cc-total.json
83 | cc-test-reporter upload-coverage -i coverage/cc-total.json
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /docs/_site
3 | /composer.lock
4 | /docs/.jekyll-cache
5 | /examples/*.sqlite
6 | /examples/*.sql
7 | /.phpunit.result.cache
8 |
--------------------------------------------------------------------------------
/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # Notes for contributors
2 |
3 | Please have in mind that this library is PSR-2 compliant. No changes can be merged that are not PSR-2 compliant.
4 |
5 | ## Testing
6 |
7 | The code should have a 100% code coverage. To run the code coverage is not necessary - it will run on pull request. If
8 | There is something not testable we either find a solution or mark it with @ignoreCodeCoverage.
9 |
10 | To run the tests you just have to install the dev dependencies with `composer install`. Then you can start both:
11 | Unit tests `composer test` and code sniffer `composer code-style`.
12 |
13 | ## Documentation
14 |
15 | You should update the documentation according to your changes.
16 |
17 | In order to update the API reference you will also have to install docker. Use this command to update the
18 | documentation:
19 |
20 | ```console
21 | $ docker run --rm --user $(id -u) -v $(pwd):/data -v $(pwd)/docs/_reference:/opt/phpdoc/data/templates/_reference iras/phpdoc2:2 phpdoc -c phpdoc.xml
22 | ```
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tflori/orm",
3 | "description": "lightweight object relational mapper",
4 | "license": "MIT",
5 | "require": {
6 | "php": "^7.3 || ^8.0",
7 | "ext-json": "*",
8 | "ext-mbstring": "*",
9 | "ext-pdo": "*"
10 | },
11 | "require-dev": {
12 | "mockery/mockery": "^1.1",
13 | "phpunit/phpunit": "*",
14 | "tflori/phpunit-printer": "*",
15 | "squizlabs/php_codesniffer": "^3.5"
16 | },
17 | "suggest": {
18 | "mockery/mockery": "^1.1"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "ORM\\": "src/"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "ORM\\Test\\": "tests/"
28 | }
29 | },
30 | "archive": {
31 | "exclude": ["/tests", "/docs", "/examples"]
32 | },
33 | "scripts": {
34 | "code-style": [
35 | "phpcs --standard=PSR2 src",
36 | "phpcs --standard=PSR2 --ignore=Examples tests"
37 | ],
38 | "coverage": "phpunit --coverage-text",
39 | "test": "phpunit --color=always"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | baseurl: /orm
2 |
--------------------------------------------------------------------------------
/docs/_data/menu.yml:
--------------------------------------------------------------------------------
1 | Introduction: /
2 | Configuration: /configuration.html
3 | Entity Definition: /entityDefinition.html
4 | Working With Entities: /entities.html
5 | Use Filters: /filters.html
6 | Relation Definition: /relationDefinition.html
7 | Working With Relations: /relations.html
8 | Events and Observers: /events.html
9 | Use QueryBuilder: /querybuilder.html
10 | Validate Data: /validate.html
11 | Bulk Inserts: /bulkInserts.html
12 | Testing: /testing.html
13 |
14 | ' ': /placeholder.html
15 |
16 | API Reference: /reference.html
17 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | tflori/orm - {{ page.title }}
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/docs/_reference/class.md.twig:
--------------------------------------------------------------------------------
1 | ### {{ node.FullyQualifiedStructuralElementName|trim('\\') }}
2 |
3 | {% if node.parent is not empty %}
4 | **Extends:** {% for parent in node.parent %}
5 | [{{ parent.FullyQualifiedStructuralElementName|trim('\\') }}](#{{ parent.FullyQualifiedStructuralElementName|replace({'\\': ''})|lower }})
6 | {% else %}[{{ node.parent.FullyQualifiedStructuralElementName|trim('\\') }}](#{{ node.parent.FullyQualifiedStructuralElementName|replace({'\\': ''})|lower }})
7 | {% endfor %}
8 | {% endif %}
9 |
10 | {% if node.interfaces is not empty %}
11 | **Implements:** {% for interface in node.interfaces %}
12 | [{{ interface.FullyQualifiedStructuralElementName|trim('\\') }}](#{{ interface.FullyQualifiedStructuralElementName|replace({'\\': ''})|lower }})
13 | {% endfor %}
14 | {% endif %}
15 |
16 | {% if node.summary is not empty and node.summary != 'Class '~node.name %}
17 | #### {{ node.summary|raw }}
18 | {% endif %}
19 |
20 | {{ node.description|raw }}
21 |
22 | {% if node.deprecated %}* **Warning:** this class is **deprecated**. This means that this class will likely be removed in a future version.
23 | {% endif %}
24 |
25 | {% if node.tags.see is not empty or node.tags.link is not empty %}
26 | **See Also:**
27 |
28 | {% for see in node.tags.see %}
29 | * {{ see.reference }} {% if see.description %}- {{ see.description|raw }}{% endif %}
30 | {% endfor %}
31 | {% for link in node.tags.link %}
32 | * [{{ link.description ?: link.link }}]({{ link.link }})
33 | {% endfor %}
34 |
35 | {% endif %}{# node.tags.see || node.tags.link #}
36 |
37 | {% if node.constants is not empty %}
38 | #### Constants
39 |
40 | | Name | Value |
41 | |------|-------|
42 | {% for constant in node.constants %}
43 | | {{ constant.name }} | `{{ constant.value|raw }}` |
44 | {% endfor %}
45 |
46 | {% endif %}
47 |
48 | {% if (node.inheritedProperties.merge(node.properties)) is not empty %}
49 | #### Properties
50 |
51 | | Visibility | Name | Type | Description |
52 | |------------|------|------|---------------------------------------|
53 | {% for property in node.inheritedProperties.merge(node.properties).sortBy('name') %}
54 | | **{{ property.visibility }}{{ property.isStatic ? ' static' }}** | `${{ property.name }}` | {% if property.types is not empty %}**{{ property.types ? property.types|join(' | ')|replace({'{% endif %}
15 | {% if method.static %}**Static:** this method is **static**.
16 |
{% endif %}
17 | **Visibility:** this method is **{{ method.visibility }}**.
18 |
19 | {% if method.name != '__construct' and method.response %} **Returns**: this method returns **{{ method.response.types[0] == 'self' ? node.name : method.response.types|join('|') }}**
20 |
{% endif %}
21 | {% if method.response.description %}**Response description:** {{ method.response.description|raw }}
22 |
{% endif %}
23 | {% if method.tags.throws is not empty %}**Throws:** this method may throw {% for throws in method.tags.throws %}
24 | {{ not loop.first ? ' or ' }}**{{ throws.types|join('** or **')|raw }}**{% endfor %}
25 |
{% endif %}
26 |
27 |
28 | {% if method.arguments is not empty %}
29 | ##### Parameters
30 |
31 | | Parameter | Type | Description |
32 | |-----------|------|-------------|
33 | {% for argument in method.arguments %}
34 | | `{{ argument.name }}` | {% if argument.types is not empty %}**{{ argument.types ? argument.types|join(' | ')|replace({'
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Introduction
4 | permalink: /
5 | ---
6 | ## Introduction
7 |
8 | **TL;DR** Others suck, we can do it better.
9 |
10 | Why to create another ORM? There are not enough ORM implementations for PHP already?
11 |
12 | Yes, there are a lot of implementations:
13 |
14 | - doctrine/orm
15 | - heavy: 8,8 MB of everything that you don't need, 6 direct dependencies with own dependencies
16 | - annotations that makes it unreadable
17 | - big amount of queries or very slow queries when it comes to join across multiple tables
18 |
19 | - eloquent
20 | - no single entry package (you need illuminate/this, that and database)
21 | - complicated setup
22 | - I **like** this very much in laravel - so not much to say here against it
23 |
24 | - propel/propel
25 | - still not stable 2.0-dev
26 | - even more heavy than doctrine
27 | - requires a lot of configurations
28 |
29 | - j4mie/idiorim and j4mie/paris
30 | - uses a lot of static methods and gets hard to test
31 | - not compatible to existing dependecy injection models
32 | - last update 2 years ago
33 | - everything in one file
34 | - ...
35 |
36 | This implementation will have the following features:
37 |
38 | - no configuration required
39 | - ok some bit for sure (e.g. how to connect to your database?)
40 | - of course this is only possible if you setup your database as we think your database should look like. If not you
41 | should only have to setup the rules of your system and naming conventions.
42 | - simple to use
43 | - lightweight sources
44 | - fast
45 |
46 | How to achieve this features? The main goal of Doctrine seems to abstract everything - at the end you should be able
47 | to replace the whole DBMS behind your app and switch from postgresql to sqlite. That requires not only a lot of
48 | sources. It also requires some extra cycles to get these abstraction to work.
49 |
50 | This library will only produce ANSI-SQL that every SQL database should understand. Other queries have to be written by
51 | hand. This has two reasons:
52 |
53 | 1. You can write much faster and efficient queries
54 | 2. We don't need to write a lot of abstraction (more code; more bugs)
55 |
56 | This library will not fetch any mistake a developer can make. It aims to be a helper to store data in your database. Not
57 | to replace your database and your knowledge how to use this database. You can make a lot of errors - less than without
58 | this library but still a lot. When you make an error that is not catched (mostly we catch only mistakes that would
59 | cause a fatal error instead) you will get a `PDOException`.
60 |
--------------------------------------------------------------------------------
/docs/javascripts/scale.fix.js:
--------------------------------------------------------------------------------
1 | var metas = document.getElementsByTagName('meta');
2 | var i;
3 | if (navigator.userAgent.match(/iPhone/i)) {
4 | for (i=0; i new ORM\DbConfig('sqlite', '/tmp/example.sqlite')
19 | ]);
20 |
21 | // reset the database
22 | $em->getConnection()->query("DROP TABLE IF EXISTS user");
23 |
24 | $em->getConnection()->query("CREATE TABLE user (
25 | id INTEGER NOT NULL PRIMARY KEY,
26 | username VARCHAR (20) NOT NULL,
27 | password VARCHAR (32) NOT NULL
28 | )");
29 |
30 | $em->getConnection()->query("CREATE UNIQUE INDEX user_username ON user (username)");
31 |
32 | $em->getConnection()->query("INSERT INTO user (username, password) VALUES
33 | ('user_a', '" . md5('password_a') . "'),
34 | ('user_b', '" . md5('password_b') . "'),
35 | ('user_c', '" . md5('password_c') . "')
36 | ");
37 |
38 | $em->getConnection()->query("DROP TABLE IF EXISTS comment");
39 | $em->getConnection()->query("CREATE TABLE comment (
40 | id INTEGER PRIMARY KEY AUTOINCREMENT,
41 | parent_type VARCHAR(50) NOT NULL,
42 | parent_id INTEGER NOT NULL,
43 | author VARCHAR(50) DEFAULT 'Anonymous' NOT NULL,
44 | text TEXT
45 | )");
46 |
47 | $em->getConnection()->query("DROP TABLE IF EXISTS article");
48 | $em->getConnection()->query("CREATE TABLE article (
49 | id INTEGER PRIMARY KEY AUTOINCREMENT,
50 | author VARCHAR(50) DEFAULT 'Anonymous' NOT NULL,
51 | title VARCHAR(255) NOT NULL,
52 | text TEXT
53 | )");
54 |
55 | $em->getConnection()->query("DROP TABLE IF EXISTS image");
56 | $em->getConnection()->query("CREATE TABLE image (
57 | id INTEGER PRIMARY KEY AUTOINCREMENT,
58 | author VARCHAR(50) DEFAULT 'Anonymous' NOT NULL,
59 | url VARCHAR(255) NOT NULL,
60 | caption VARCHAR(255)
61 | )");
62 |
--------------------------------------------------------------------------------
/examples/entities.php:
--------------------------------------------------------------------------------
1 | username));
19 | }
20 | }
21 |
22 | class Comment extends ORM\Entity
23 | {
24 | protected static $relations = [
25 | 'parent' => [['parentType' => [
26 | 'article' => Article::class,
27 | 'image' => Image::class,
28 | ]], ['parentId' => 'id']],
29 | ];
30 | }
31 |
32 | class Article extends ORM\Entity
33 | {
34 | protected static $relations = [
35 | 'comments' => [Comment::class, 'parent'],
36 | ];
37 | }
38 |
39 | class Image extends ORM\Entity
40 | {
41 | protected static $relations = [
42 | 'comments' => [Comment::class, 'parent'],
43 | ];
44 | }
45 |
--------------------------------------------------------------------------------
/examples/fetch.php:
--------------------------------------------------------------------------------
1 | fetch(User::class)
17 | ->setQuery("SELECT * FROM user WHERE username = ? AND password = ?", [$username, md5($password)])
18 | ->one();
19 |
20 | var_dump($user);
21 |
22 |
23 | /*******************************
24 | * Fetch with where conditions *
25 | *******************************/
26 | $user = $em->fetch(User::class)
27 | ->where('username', 'LIKE', $username)
28 | ->andWhere('password', '=', md5($password))
29 | ->one();
30 |
31 | var_dump($user);
32 |
33 | /*******************************************
34 | * Fetch with parenthesis, group and order *
35 | *******************************************/
36 | try {
37 | $fetcher = $em->fetch(User::class)
38 | ->where(User::class . '::username LIKE ?', 'USER_A')
39 | ->andWhere('password', '=', md5('password_a'))
40 | ->orParenthesis()
41 | ->where(User::class . '::username = ' . $em->getConnection()->quote('user_b'))
42 | ->andWhere('t0.password = \'' . md5('password_b') . '\'')
43 | ->close()
44 | ->groupBy('id')
45 | ->orderBy(
46 | 'CASE WHEN username = ? THEN 1 WHEN username = ? THEN 2 ELSE 3 END',
47 | 'ASC',
48 | ['user_a', 'user_b']
49 | );
50 | $users = $fetcher->all();
51 |
52 | var_dump($users);
53 | } catch (\PDOException $exception) {
54 | file_put_contents('php://stderr', $exception->getMessage() . "\nSQL:" . $fetcher->getQuery());
55 | }
56 |
57 | /*******************
58 | * Cache an entity *
59 | *******************/
60 | $cachedUser = serialize($user);
61 | var_dump($cachedUser);
62 |
63 | /******************************
64 | * Get previously cached User *
65 | ******************************/
66 | // lets say we cached user3 with password from user1 - so modify the $cachedUser
67 | $cachedUser = str_replace([
68 | 's:1:"1"',
69 | 's:6:"user_a"'
70 | ], [
71 | 's:1:"3"',
72 | 's:6:"user_c"'
73 | ], $cachedUser);
74 | /** @var User $user */
75 | $user = $em->map(unserialize($cachedUser));
76 | $user = $em->fetch(User::class, 3);
77 | var_dump($user, $user->isDirty(), $user->isDirty('username'));
78 |
79 | /*********************************
80 | * Get a previously fetched user *
81 | *********************************/
82 | // sqlite returns strings and currently we do not convert to int
83 | $user1 = $em->fetch(User::class, 1); // queries the database again
84 | $user2 = $em->map(new User(['id' => 1]));
85 | $user3 = $em->fetch(User::class, 1); // returns $user2
86 | $user4 = $em->map(new User(['id' => '1']));
87 | var_dump($user1->username, $user2->username, $user3 === $user2, $user1 === $user4);
88 |
89 | /********************************
90 | * Validate data for a new user *
91 | ********************************/
92 | $data = [
93 | 'username' => 'This username is way to long for a username',
94 | 'password' => null // null is not allowed
95 | ];
96 | $result = User::validateArray($data);
97 | echo $result['username']->getMessage() . "\n" . $result['password']->getMessage() . "\n";
98 |
--------------------------------------------------------------------------------
/examples/filter.php:
--------------------------------------------------------------------------------
1 | column = $column;
20 | }
21 |
22 | public function getColumn()
23 | {
24 | return $this->column;
25 | }
26 |
27 | public function prepareSearchTerm($searchTerm)
28 | {
29 | return '%' . $searchTerm . '%';
30 | }
31 |
32 | public function getOperator()
33 | {
34 | return 'LIKE';
35 | }
36 | }
37 |
38 | class Text extends SearchColumn
39 | {
40 | }
41 |
42 | class FilterBySearchTerm implements FilterInterface
43 | {
44 | /** @var string[]|SearchColumn[] */
45 | protected $searchColumns;
46 |
47 | /** @var string */
48 | private $searchTerm;
49 |
50 | /**
51 | * FilterBySearchTerm constructor.
52 | * @param string[]|SearchColumn[] $searchColumns
53 | * @param string $searchTerm
54 | */
55 | public function __construct(array $searchColumns, $searchTerm)
56 | {
57 | $this->searchColumns = $searchColumns;
58 | $this->searchTerm = $searchTerm;
59 | }
60 |
61 | public function apply(EntityFetcher $fetcher)
62 | {
63 | $searchTerms = preg_split('/\s+/', $this->searchTerm);
64 | foreach ($searchTerms as $searchTerm) {
65 | $parenthesis = $fetcher->parenthesis();
66 | foreach ($this->searchColumns as $key => $column) {
67 | if (is_string($column)) {
68 | $column = $this->searchColumns[$key] = new Text($column);
69 | }
70 | $parenthesis->orWhere(
71 | $column->getColumn(),
72 | $column->getOperator(),
73 | $column->prepareSearchTerm($searchTerm)
74 | );
75 | }
76 | $parenthesis->close();
77 | }
78 | }
79 | }
80 |
81 | // maybe a stpuid example and you probably want to get the query from the request
82 | $query = 'john doe';
83 | $fetcher = $em->fetch(User::class)->filter(new FilterBySearchTerm(['username', 'password'], $query));
84 | // creates a query similar to this:
85 | // SELECT * FROM table
86 | // WHERE (username LIKE '%john%' OR password LIKE '%john%') AND (username LIKE '%doe%' OR password LIKE '%doe%')
87 |
--------------------------------------------------------------------------------
/examples/morphed.php:
--------------------------------------------------------------------------------
1 | 'iRaS',
13 | 'title' => 'The advantages of tflori/orm',
14 | 'text' => 'it is pretty obvious why this orm is better than others',
15 | ]);
16 | $article->save();
17 | $image = new Image([
18 | 'author' => 'iRaS',
19 | 'url' => 'https://cdn.business2community.com/wp-content/uploads/2013/09/best-press-release-example.jpg',
20 | 'caption' => 'This is just an example',
21 | ]);
22 | $image->save();
23 |
24 | // create some comments
25 | $texts = [
26 | 'Quod erat demonstrandum.',
27 | 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.',
28 | 'Aenean commodo ligula eget dolor. Aenean massa.',
29 | 'Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.',
30 | 'Er hörte leise Schritte hinter sich. Das bedeutete nichts Gutes.',
31 | 'Weit hinten, hinter den Wortbergen, fern der Länder Vokalien und Konsonantien leben die Blindtexte.',
32 | 'Abgeschieden wohnen sie in Buchstabhausen an der Küste des Semantik, eines großen Sprachozeans.',
33 | '204 § ab dem Jahr 2034 Zahlen in 86 der Texte zur Pflicht werden.',
34 | 'Dies ist ein Typoblindtext.',
35 | 'Vogel Quax zwickt Johnys Pferd Bim.',
36 | ];
37 | $authors = [
38 | 'iRaS',
39 | 'cat',
40 | 's1mple',
41 | ];
42 | foreach ([$article, $image] as $parent) {
43 | $count = mt_rand(2, 5);
44 | $em->useBulkInserts(Comment::class);
45 | for ($i = 0; $i < $count; $i++) {
46 | $comment = new Comment();
47 | $comment->text = $texts[array_rand($texts)];
48 | $comment->author = $authors[array_rand($authors)];
49 | $comment->setRelated('parent', $parent);
50 | $comment->save();
51 | }
52 | $em->finishBulkInserts(Comment::class);
53 | }
54 |
55 | printf('Article "%s" has %d comments:' . PHP_EOL, $article->title, count($article->comments));
56 | foreach ($article->comments as $comment) {
57 | printf(' %s: %s' . PHP_EOL, $comment->author, $comment->text);
58 | }
59 |
60 | printf('Image "%s" has %d comments:'. PHP_EOL, $image->caption, count($image->comments));
61 | foreach ($image->comments as $comment) {
62 | printf(' %s: %s'. PHP_EOL, $comment->author, $comment->text);
63 | }
64 |
--------------------------------------------------------------------------------
/phpdoc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | /tmp/phpdoc/orm
5 |
6 |
7 | docs
8 |
9 |
10 |
11 |
12 |
13 | src
14 |
15 |
16 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 | ./src/
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Dbal/Error.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class Error extends Exception
15 | {
16 | const ERROR_CODE = 'UNKNOWN';
17 |
18 | /** @var string */
19 | protected $message = 'ERROR(%code%) occurred';
20 |
21 | /** @var string */
22 | protected $errorCode;
23 |
24 | /**
25 | * Error constructor
26 | *
27 | * @param array $params
28 | * @param null $code
29 | * @param null $message
30 | * @param Error $previous
31 | */
32 | public function __construct(array $params = [], $code = null, $message = null, Error $previous = null)
33 | {
34 | $this->message = $message ?: $this->message;
35 | $this->code = $code ?: static::ERROR_CODE;
36 |
37 | $params['code'] = $this->code;
38 |
39 | $namer = EntityManager::getInstance()->getNamer();
40 |
41 | parent::__construct($namer->substitute($this->message, $params), 0, $previous);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Dbal/Error/InvalidJson.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class InvalidJson extends Error
14 | {
15 | const ERROR_CODE = 'INVALID_JSON';
16 |
17 | /** @var string */
18 | protected $message = '\'%value%\' is not a valid JSON string';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NoBoolean.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoBoolean extends Error
14 | {
15 | const ERROR_CODE = 'NO_BOOLEAN';
16 |
17 | /** @var string */
18 | protected $message = '%value% can not be converted to boolean';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NoDateTime.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoDateTime extends Error
14 | {
15 | const ERROR_CODE = 'NO_DATETIME';
16 |
17 | /** @var string */
18 | protected $message = '%value% is not a valid date or date time expression';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NoNumber.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoNumber extends Error
14 | {
15 | const ERROR_CODE = 'NO_NUMBER';
16 |
17 | /** @var string */
18 | protected $message = '%value% is not numeric';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NoString.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoString extends Error
14 | {
15 | const ERROR_CODE = 'NO_STRING';
16 |
17 | /** @var string */
18 | protected $message = 'Only string values are allowed for %type%';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NoTime.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoTime extends Error
14 | {
15 | const ERROR_CODE = 'NO_TIME';
16 |
17 | /** @var string */
18 | protected $message = '%value% is not a valid time expression';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NotAllowed.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NotAllowed extends Error
14 | {
15 | const ERROR_CODE = 'NOT_ALLOWED';
16 |
17 | /** @var string */
18 | protected $message = '\'%value%\' is not allowed by this %type%';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NotNullable.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class NotNullable extends Error
15 | {
16 | const ERROR_CODE = 'NOT_NULLABLE';
17 |
18 | protected $message = '%column% does not allow null values';
19 |
20 | public function __construct(Column $column)
21 | {
22 | parent::__construct([ 'column' => $column->name ]);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Dbal/Error/NotValid.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class NotValid extends Error
15 | {
16 | const ERROR_CODE = 'NOT_VALID';
17 |
18 | /** @var string */
19 | protected $message = 'Value not valid for %column% (Caused by: %previous%)';
20 |
21 | /**
22 | * NotValid constructor
23 | *
24 | * @param Column $column The column that got a not valid error
25 | * @param Error $previous The error from validate
26 | */
27 | public function __construct(Column $column, Error $previous)
28 | {
29 | parent::__construct([
30 | 'column' => $column->name,
31 | 'previous' => $previous->getMessage()
32 | ], null, null, $previous);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Dbal/Error/TooLong.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class TooLong extends Error
14 | {
15 | const ERROR_CODE = 'TOO_LONG';
16 |
17 | /** @var string */
18 | protected $message = '\'%value%\' is too long (max: %max%)';
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dbal/Escaping.php:
--------------------------------------------------------------------------------
1 | quotingCharacter;
33 | $divider = $this->identifierDivider;
34 | return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote;
35 | }
36 |
37 | /**
38 | * Returns $value formatted to use in a sql statement.
39 | *
40 | * @param mixed $value The variable that should be returned in SQL syntax
41 | * @return string
42 | * @throws NotScalar
43 | */
44 | public function escapeValue($value)
45 | {
46 | if ($value instanceof DateTime) {
47 | return $this->escapeDateTime($value);
48 | }
49 |
50 | if ($value instanceof Expression) {
51 | return (string)$value;
52 | }
53 |
54 | $type = is_object($value) ? get_class($value) : gettype($value);
55 | $method = [ $this, 'escape' . ucfirst($type) ];
56 |
57 | if (is_callable($method)) {
58 | return call_user_func($method, $value);
59 | } else {
60 | throw new NotScalar('$value has to be scalar data type. ' . gettype($value) . ' given');
61 | }
62 | }
63 |
64 | /**
65 | * Escape a string for query
66 | *
67 | * @param string $value
68 | * @return string
69 | */
70 | protected function escapeString($value)
71 | {
72 | return $this->entityManager->getConnection()->quote($value);
73 | }
74 |
75 | /**
76 | * Escape an integer for query
77 | *
78 | * @param int $value
79 | * @return string
80 | */
81 | protected function escapeInteger($value)
82 | {
83 | return (string) $value;
84 | }
85 |
86 | /**
87 | * Escape a double for Query
88 | *
89 | * @param double $value
90 | * @return string
91 | */
92 | protected function escapeDouble($value)
93 | {
94 | return (string) $value;
95 | }
96 |
97 | /**
98 | * Escape NULL for query
99 | *
100 | * @return string
101 | */
102 | protected function escapeNULL()
103 | {
104 | return 'NULL';
105 | }
106 |
107 | /**
108 | * Escape a boolean for query
109 | *
110 | * @param bool $value
111 | * @return string
112 | */
113 | protected function escapeBoolean($value)
114 | {
115 | return ($value) ? $this->booleanTrue : $this->booleanFalse;
116 | }
117 |
118 | /**
119 | * Escape a date time object for query
120 | *
121 | * @param DateTime $value
122 | * @return mixed
123 | */
124 | protected function escapeDateTime(DateTime $value)
125 | {
126 | $value->setTimezone(new DateTimeZone('UTC'));
127 | return $this->escapeString($value->format('Y-m-d\TH:i:s.u\Z'));
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Dbal/Expression.php:
--------------------------------------------------------------------------------
1 | expression = $expression;
17 | }
18 |
19 | public function __toString()
20 | {
21 | return $this->expression;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Dbal/Other.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class Other extends Dbal
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/CompositeInExpression.php:
--------------------------------------------------------------------------------
1 | buildTuples($values)) . ')';
19 | }
20 |
21 | protected function buildTuples(array $values)
22 | {
23 | return array_map(function ($value) {
24 | return '(' . implode(',', array_map([$this, 'escapeValue'], $value)) . ')';
25 | }, $values);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/CompositeInValuesExpression.php:
--------------------------------------------------------------------------------
1 | buildTuples($values)) . ')';
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/DeleteStatement.php:
--------------------------------------------------------------------------------
1 | escapeIdentifier($table) .
12 | $this->buildWhereClause($where);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/InsertStatement.php:
--------------------------------------------------------------------------------
1 | escapeIdentifier($table) . ' ' .
23 | '(' . implode(',', array_map([$this, 'escapeIdentifier'], $columns)) . ') VALUES ';
24 |
25 | $statement .= implode(',', array_map(function ($values) use ($columns) {
26 | return '(' . implode(',', array_map(function ($column) use ($values) {
27 | return isset($values[$column]) ? $this->escapeValue($values[$column]) : $this->escapeNULL();
28 | }, $columns)) . ')';
29 | }, $rows));
30 |
31 | return $statement;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/UpdateFromStatement.php:
--------------------------------------------------------------------------------
1 | convertJoin(array_shift($joins));
16 | $fromClause = ' FROM ' . $fromTable .
17 | (!empty($joins) ? ' ' . implode(' ', $joins) : '');
18 | array_unshift($where, $condition);
19 | }
20 |
21 | return 'UPDATE ' . $this->escapeIdentifier($table) .
22 | $this->buildSetClause($updates) .
23 | $fromClause .
24 | $this->buildWhereClause($where);
25 | }
26 |
27 | protected function convertJoin($join)
28 | {
29 | if (!preg_match('/^JOIN\s+([^\s]+)\s+ON\s+(.*)/ism', $join, $match)) {
30 | throw new Exception\InvalidArgument(
31 | 'Only inner joins with on clause are allowed in update from statements'
32 | );
33 | }
34 | $table = $match[1];
35 | $condition = $match[2];
36 |
37 | return [$table, $condition];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/UpdateJoinStatement.php:
--------------------------------------------------------------------------------
1 | escapeIdentifier($table) .
12 | (!empty($joins) ? ' ' . implode(' ', $joins) : '') .
13 | $this->buildSetClause($updates) .
14 | $this->buildWhereClause($where);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/UpdateStatement.php:
--------------------------------------------------------------------------------
1 | escapeIdentifier($table) .
12 | $this->buildSetClause($updates) .
13 | $this->buildWhereClause($where);
14 | }
15 |
16 | protected function buildSetClause(array $updates)
17 | {
18 | return ' SET ' . implode(',', array_map(function ($column, $value) {
19 | return $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($value);
20 | }, array_keys($updates), $updates));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Dbal/QueryLanguage/WhereClause.php:
--------------------------------------------------------------------------------
1 | $condition) {
15 | if (!is_numeric($column)) {
16 | $condition = $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($condition);
17 | }
18 |
19 | if (!empty($normalized) && !preg_match('/^\s*(AND|OR)/i', $condition)) {
20 | $condition = 'AND ' . $condition;
21 | }
22 |
23 | $normalized[] = trim($condition);
24 | }
25 |
26 | return ' WHERE ' . implode(' ', $normalized);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Dbal/Table.php:
--------------------------------------------------------------------------------
1 |
13 | * @method Column offsetGet($offset)
14 | */
15 | class Table extends ArrayObject
16 | {
17 | /** The columns from this table
18 | * @var Column[] */
19 | protected $columns;
20 |
21 | /**
22 | * Table constructor.
23 | *
24 | * @param Column[] $columns
25 | */
26 | public function __construct(array $columns)
27 | {
28 | foreach ($columns as $column) {
29 | $this->columns[$column->name] = $column;
30 | }
31 |
32 | parent::__construct($columns);
33 | }
34 |
35 | /**
36 | * Validate $value for column $col.
37 | *
38 | * Returns an array with at least
39 | *
40 | * @param string $col
41 | * @param mixed $value
42 | * @return bool|Error
43 | * @throws UnknownColumn
44 | */
45 | public function validate($col, $value)
46 | {
47 | if (!($column = $this->getColumn($col))) {
48 | throw new UnknownColumn('Unknown column ' . $col);
49 | }
50 |
51 | return $column->validate($value);
52 | }
53 |
54 | /**
55 | * Get the Column object for $col
56 | *
57 | * @param string $col
58 | * @return Column
59 | */
60 | public function getColumn($col)
61 | {
62 | return isset($this->columns[$col]) ? $this->columns[$col] : null;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Dbal/Type.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | abstract class Type implements TypeInterface
12 | {
13 | /**
14 | * {@inheritdoc}
15 | * @codeCoverageIgnore void method for types covered by mapping
16 | */
17 | public static function fits(array $columnDefinition)
18 | {
19 | return false;
20 | }
21 |
22 | /**
23 | * Returns a new Type object
24 | *
25 | * This method is only for types covered by mapping. Use fromDefinition instead for custom types.
26 | *
27 | * @param Dbal $dbal
28 | * @param array $columnDefinition
29 | * @return static
30 | */
31 | public static function factory(Dbal $dbal, array $columnDefinition)
32 | {
33 | return new static();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Boolean.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class Boolean extends Type
17 | {
18 | /** @var Dbal */
19 | protected $dbal;
20 |
21 | /**
22 | * Boolean constructor
23 | *
24 | * @param Dbal $dbal
25 | */
26 | public function __construct(Dbal $dbal)
27 | {
28 | $this->dbal = $dbal;
29 | }
30 |
31 | public static function factory(Dbal $dbal, array $columnDefinition)
32 | {
33 | return new static($dbal);
34 | }
35 |
36 | /**
37 | * Check if $value is valid for this type
38 | *
39 | * @param mixed $value
40 | * @return boolean|Error
41 | */
42 | public function validate($value)
43 | {
44 | if (!is_bool($value)) {
45 | // convert int to string
46 | if (is_int($value)) {
47 | $value = (string) $value;
48 | }
49 |
50 | if (!is_string($value) ||
51 | ($value !== $this->getBoolean(true) && $value !== $this->getBoolean(false))
52 | ) {
53 | // value is not boolean, not int and (not string OR string value for boolean)
54 | return new NoBoolean([ 'value' => (string) $value ]);
55 | }
56 | }
57 |
58 | return true;
59 | }
60 |
61 | /**
62 | * Get the string representation for boolean
63 | *
64 | * @param bool $bool
65 | * @return string
66 | */
67 | protected function getBoolean($bool)
68 | {
69 | $quoted = $this->dbal->escapeValue($bool);
70 | return $quoted[0] === '\'' ? substr($quoted, 1, -1) : $quoted;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Dbal/Type/DateTime.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class DateTime extends Type
17 | {
18 | const DATE_REGEX = '(\+|-)?\d{4,}-\d{2}-\d{2}';
19 | const TIME_REGEX = '\d{2}:\d{2}:\d{2}(\.\d{1,6})?';
20 | const ZONE_REGEX = '((\+|-)\d{1,2}(:?\d{2})?|Z)?';
21 |
22 | /** @var int */
23 | protected $precision;
24 |
25 | /** @var string */
26 | protected $regex;
27 |
28 | /**
29 | * DateTime constructor
30 | *
31 | * @param int $precision
32 | * @param bool $dateOnly
33 | */
34 | public function __construct($precision = null, $dateOnly = false)
35 | {
36 | $this->precision = (int) $precision;
37 | $this->regex = $dateOnly ?
38 | '/^' . self::DATE_REGEX . '([ T]' . self::TIME_REGEX . self::ZONE_REGEX . ')?$/' :
39 | '/^' . self::DATE_REGEX . '[ T]' . self::TIME_REGEX . self::ZONE_REGEX . '$/';
40 | }
41 |
42 | public static function factory(Dbal $dbal, array $columnDefinition)
43 | {
44 | return new static(
45 | $columnDefinition['datetime_precision'],
46 | strpos($columnDefinition['data_type'], 'time') === false
47 | );
48 | }
49 |
50 | /**
51 | * Check if $value is valid for this type
52 | *
53 | * @param mixed $value
54 | * @return boolean|Error
55 | */
56 | public function validate($value)
57 | {
58 | if (!$value instanceof \DateTime && (!is_string($value) || !preg_match($this->regex, $value))) {
59 | return new NoDateTime([ 'value' => (string) $value ]);
60 | }
61 |
62 | return true;
63 | }
64 |
65 | /**
66 | * @return int
67 | */
68 | public function getPrecision()
69 | {
70 | return $this->precision;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Enum.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class Enum extends Type
18 | {
19 | /** @var string[] */
20 | protected $allowedValues = null;
21 |
22 | /**
23 | * Set constructor
24 | *
25 | * @param string[] $allowedValues
26 | */
27 | public function __construct(array $allowedValues)
28 | {
29 | $this->allowedValues = $allowedValues;
30 | }
31 |
32 | public static function factory(Dbal $dbal, array $columnDefinition)
33 | {
34 | $allowedValues = [];
35 | if (!empty($columnDefinition['enumeration_values'])) {
36 | $allowedValues = explode('\',\'', substr($columnDefinition['enumeration_values'], 1, -1));
37 | }
38 |
39 | return new static($allowedValues);
40 | }
41 |
42 | /**
43 | * Check if $value is valid for this type
44 | *
45 | * @param mixed $value
46 | * @return boolean|Error
47 | */
48 | public function validate($value)
49 | {
50 | if (!is_string($value)) {
51 | return new NoString([ 'type' => 'enum' ]);
52 | } elseif (!in_array($value, $this->allowedValues)) {
53 | return new NotAllowed([ 'value' => $value, 'type' => 'enum' ]);
54 | }
55 |
56 | return true;
57 | }
58 |
59 | /**
60 | * @return string[]
61 | */
62 | public function getAllowedValues()
63 | {
64 | return $this->allowedValues;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Json.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class Json extends Type
17 | {
18 | /**
19 | * Check if $value is valid for this type
20 | *
21 | * @param mixed $value
22 | * @return boolean|Error
23 | */
24 | public function validate($value)
25 | {
26 | if (!is_string($value)) {
27 | return new NoString([ 'type' => 'json' ]);
28 | } elseif ($value !== 'null' && json_decode($value) === null) {
29 | return new InvalidJson([ 'value' => (string) $value ]);
30 | }
31 |
32 | return true;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Number.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class Number extends Type
16 | {
17 | /**
18 | * Check if $value is valid for this type
19 | *
20 | * @param mixed $value
21 | * @return boolean|Error
22 | */
23 | public function validate($value)
24 | {
25 | if (!is_int($value) && !is_double($value) && (!is_string($value) || !is_numeric($value))) {
26 | return new NoNumber([ 'value' => (string) $value ]);
27 | }
28 |
29 | return true;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Set.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class Set extends Enum
16 | {
17 | protected $type = 'set';
18 |
19 | /**
20 | * Check if $value is valid for this type
21 | *
22 | * @param mixed $value
23 | * @return boolean|Error
24 | */
25 | public function validate($value)
26 | {
27 | if (!is_string($value)) {
28 | return new NoString([ 'type' => 'set' ]);
29 | } else {
30 | $values = explode(',', $value);
31 | foreach ($values as $value) {
32 | if (!in_array($value, $this->allowedValues)) {
33 | return new NotAllowed([ 'value' => $value, 'type' => 'set' ]);
34 | }
35 | }
36 | }
37 |
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Text.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class Text extends VarChar
14 | {
15 | protected $type = 'text';
16 | }
17 |
--------------------------------------------------------------------------------
/src/Dbal/Type/Time.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class Time extends DateTime
15 | {
16 | public function __construct($precision = null)
17 | {
18 | parent::__construct($precision);
19 | $this->regex = '/^' . self::TIME_REGEX . self::ZONE_REGEX . '$/';
20 | }
21 |
22 | /**
23 | * Check if $value is valid for this type
24 | *
25 | * @param mixed $value
26 | * @return boolean|Error
27 | */
28 | public function validate($value)
29 | {
30 | if (!is_string($value) || !preg_match($this->regex, $value)) {
31 | if ($value instanceof \DateTime) {
32 | return new Error([], 'DATETIME', 'DateTime is not allowed for time');
33 | }
34 |
35 | return new NoTime([ 'value' => (string) $value ]);
36 | }
37 |
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Dbal/Type/VarChar.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class VarChar extends Type
20 | {
21 | /** @var int */
22 | protected $maxLength;
23 |
24 | /** @var string */
25 | protected $type = 'varchar';
26 |
27 | /**
28 | * VarChar constructor
29 | *
30 | * @param int $maxLength
31 | */
32 | public function __construct($maxLength = null)
33 | {
34 | $this->maxLength = (int) $maxLength;
35 | }
36 |
37 | public static function factory(Dbal $dbal, array $columnDefinition)
38 | {
39 | return new static($columnDefinition['character_maximum_length']);
40 | }
41 |
42 | /**
43 | * Check if $value is valid for this type
44 | *
45 | * @param mixed $value
46 | * @return boolean|Error
47 | */
48 | public function validate($value)
49 | {
50 | if (!is_string($value)) {
51 | return new NoString([ 'type' => $this->type ]);
52 | } elseif ($this->maxLength !== 0 && mb_strlen($value) > $this->maxLength) {
53 | return new TooLong([ 'value' => $value, 'max' => $this->maxLength ]);
54 | }
55 |
56 | return true;
57 | }
58 |
59 | /**
60 | * @return int
61 | */
62 | public function getMaxLength()
63 | {
64 | return $this->maxLength;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Dbal/TypeInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | interface TypeInterface
12 | {
13 | /**
14 | * Create Type class for given $dbal and $columnDefinition
15 | *
16 | * @param Dbal $dbal
17 | * @param array $columnDefinition
18 | * @return Type
19 | */
20 | public static function factory(Dbal $dbal, array $columnDefinition);
21 |
22 | /**
23 | * Check if this type fits to $columnDefinition
24 | *
25 | * @param array $columnDefinition
26 | * @return boolean
27 | */
28 | public static function fits(array $columnDefinition);
29 |
30 | /**
31 | * Check if $value is valid for this type
32 | *
33 | * @param mixed $value
34 | * @return boolean|Error
35 | */
36 | public function validate($value);
37 | }
38 |
--------------------------------------------------------------------------------
/src/EM.php:
--------------------------------------------------------------------------------
1 | getNamer()
41 | ->getMethodName('boot' . ucfirst(Helper::shortName($trait)));
42 | if (method_exists(static::class, $method)) {
43 | forward_static_call([static::class, $method]);
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Entity/EventHandlers.php:
--------------------------------------------------------------------------------
1 | describe(static::getTableName());
65 | }
66 |
67 | /**
68 | * Validate $value for $attribute
69 | *
70 | * @param string $attribute
71 | * @param mixed $value
72 | * @return bool|Error
73 | */
74 | public static function validate($attribute, $value)
75 | {
76 | return static::describe()->validate(static::getColumnName($attribute), $value);
77 | }
78 |
79 | /**
80 | * Validate $data
81 | *
82 | * $data has to be an array of $attribute => $value
83 | *
84 | * @param array $data
85 | * @return array
86 | */
87 | public static function validateArray(array $data)
88 | {
89 | $result = $data;
90 | foreach ($result as $attribute => &$value) {
91 | $value = static::validate($attribute, $value);
92 | }
93 | return $result;
94 | }
95 |
96 | /**
97 | * Check if the current data is valid
98 | *
99 | * Returns boolean true when valid otherwise an array of Errors.
100 | *
101 | * @return bool|Error[]
102 | */
103 | public function isValid()
104 | {
105 | $result = [];
106 |
107 | $presentColumns = [];
108 | foreach ($this->data as $column => $value) {
109 | $presentColumns[] = $column;
110 | $result[] = static::validate($column, $value);
111 | }
112 |
113 | foreach (static::describe() as $column) {
114 | if (!in_array($column->name, $presentColumns)) {
115 | $result[] = static::validate($column->name, null);
116 | }
117 | }
118 |
119 | $result = array_values(array_filter($result, function ($error) {
120 | return $error instanceof Error;
121 | }));
122 |
123 | return count($result) === 0 ? true : $result;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/EntityFetcher/CallableFilter.php:
--------------------------------------------------------------------------------
1 | filter = $filter;
17 | }
18 |
19 | /** {@inheritDoc} */
20 | public function apply(EntityFetcher $fetcher)
21 | {
22 | call_user_func($this->filter, $fetcher);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/EntityFetcher/ExecutesQueries.php:
--------------------------------------------------------------------------------
1 | class, 'getColumnName'], array_keys($updates)),
22 | array_values($updates)
23 | );
24 | return parent::update($updates);
25 | }
26 |
27 | /**
28 | * Execute an insert statement on the current table
29 | *
30 | * @param array ...$rows
31 | * @return int The number of inserted rows
32 | */
33 | public function insert(array ...$rows)
34 | {
35 | $rows = array_map(function ($row) {
36 | return array_combine(
37 | array_map([$this->class, 'getColumnName'], array_keys($row)),
38 | array_values($row)
39 | );
40 | }, $rows);
41 | return parent::insert(...$rows);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/EntityFetcher/FilterInterface.php:
--------------------------------------------------------------------------------
1 | getTableAndAlias($class, $alias);
12 | return parent::join($table, $expression, $alias, $args);
13 | }
14 |
15 | public function leftJoin($class, $expression = '', $alias = '', $args = [])
16 | {
17 | list($table, $alias) = $this->getTableAndAlias($class, $alias);
18 | return parent::leftJoin($table, $expression, $alias, $args);
19 | }
20 |
21 | public function rightJoin($class, $expression = '', $alias = '', $args = [])
22 | {
23 | list($table, $alias) = $this->getTableAndAlias($class, $alias);
24 | return parent::rightJoin($table, $expression, $alias, $args);
25 | }
26 |
27 | public function fullJoin($class, $expression = '', $alias = '', $args = [])
28 | {
29 | list($table, $alias) = $this->getTableAndAlias($class, $alias);
30 | return parent::fullJoin($table, $expression, $alias, $args);
31 | }
32 |
33 | /**
34 | * Create the join with $join type
35 | *
36 | * @param $join
37 | * @param $relation
38 | * @return $this
39 | */
40 | public function createRelatedJoin($join, $relation)
41 | {
42 | if (strpos($relation, '.') !== false) {
43 | list($alias, $relation) = explode('.', $relation);
44 | $class = $this->classMapping['byAlias'][$alias];
45 | } else {
46 | $class = $this->class;
47 | $alias = $this->alias;
48 | }
49 |
50 | /** @var Relation $relation */
51 | $relation = call_user_func([$class, 'getRelation'], $relation);
52 | $relation->addJoin($this, $join, $alias);
53 | return $this;
54 | }
55 |
56 | /**
57 | * Join $relation
58 | *
59 | * @param $relation
60 | * @return $this
61 | */
62 | public function joinRelated($relation)
63 | {
64 | return $this->createRelatedJoin('join', $relation);
65 | }
66 |
67 | /**
68 | * Left outer join $relation
69 | *
70 | * @param $relation
71 | * @return $this
72 | */
73 | public function leftJoinRelated($relation)
74 | {
75 | return $this->createRelatedJoin('leftJoin', $relation);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/EntityFetcher/TranslatesClasses.php:
--------------------------------------------------------------------------------
1 | [],
14 | 'byAlias' => [],
15 | ];
16 |
17 | /**
18 | * Translate attribute names in an expression to their column names
19 | *
20 | * @param string $expression
21 | * @return string
22 | * @throws NotJoined
23 | */
24 | protected function translateColumn($expression)
25 | {
26 | return preg_replace_callback(
27 | '/(?^| |\()' .
28 | '((?[A-Za-z_][A-Za-z0-9_\\\\]*)::|(?[A-Za-z_][A-Za-z0-9_]+)\.)?' .
29 | '(?[A-Za-z_][A-Za-z0-9_]*)' .
30 | '(?$| |,|\))/',
31 | function ($match) {
32 | if ($match['alias'] && !isset($this->classMapping['byAlias'][$match['alias']])) {
33 | return $match[0];
34 | } elseif ($match['column'] === strtoupper($match['column'])) {
35 | return $match['b'] . $match['column'] . $match['a'];
36 | }
37 |
38 | list($class, $alias) = $this->toClassAndAlias($match);
39 |
40 | /** @var Entity|string $class */
41 | return $match['b'] . $this->entityManager->escapeIdentifier(
42 | $alias . '.' . $class::getColumnName($match['column'])
43 | ) . $match['a'];
44 | },
45 | $expression
46 | );
47 | }
48 |
49 | /**
50 | * Get class and alias by the match from translateColumn
51 | *
52 | * @param array $match
53 | * @return array [$class, $alias]
54 | * @throws NotJoined
55 | */
56 | private function toClassAndAlias(array $match)
57 | {
58 | if ($match['class']) {
59 | if (!isset($this->classMapping['byClass'][$match['class']])) {
60 | throw new NotJoined("Class " . $match['class'] . " not joined");
61 | }
62 | $class = $match['class'];
63 | $alias = $this->classMapping['byClass'][$match['class']];
64 | } elseif ($match['alias']) {
65 | $alias = $match['alias'];
66 | $class = $this->classMapping['byAlias'][$match['alias']];
67 | } else {
68 | $class = $this->class;
69 | $alias = $this->alias;
70 | }
71 |
72 | return [$class, $alias];
73 | }
74 |
75 | /**
76 | * Get the table name and alias for a class
77 | *
78 | * @param string $class
79 | * @param string $alias
80 | * @return array [$table, $alias]
81 | */
82 | protected function getTableAndAlias($class, $alias = '')
83 | {
84 | if (class_exists($class)) {
85 | /** @var Entity|string $class */
86 | $table = $this->entityManager->escapeIdentifier($class::getTableName());
87 | $alias = $alias ?: 't' . count($this->classMapping['byAlias']);
88 |
89 | $this->classMapping['byClass'][$class] = $alias;
90 | $this->classMapping['byAlias'][$alias] = $class;
91 | } else {
92 | $table = $class;
93 | }
94 |
95 | return [$table, $alias];
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Event.php:
--------------------------------------------------------------------------------
1 | entity = $entity;
29 | $this->data = $entity->getData();
30 | }
31 |
32 | public function __get($name)
33 | {
34 | return isset($this->$name) ? $this->$name : null;
35 | }
36 |
37 | public function stop()
38 | {
39 | $this->stopped = true;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Event/Changed.php:
--------------------------------------------------------------------------------
1 | attribute = $attribute;
31 | $this->oldValue = $oldValue;
32 | $this->newValue = $newValue;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Event/Deleted.php:
--------------------------------------------------------------------------------
1 | rawData = $rawData;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Event/Inserted.php:
--------------------------------------------------------------------------------
1 | entity);
20 |
21 | $this->originalEvent = $originalEvent;
22 | }
23 |
24 | public function __get($name)
25 | {
26 | return isset($this->$name) ? $this->$name : $this->originalEvent->$name;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Event/Saving.php:
--------------------------------------------------------------------------------
1 | dirty = $dirty;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Event/Updated.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class Exception extends \Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/IncompletePrimaryKey.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class IncompletePrimaryKey extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgument.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class InvalidArgument extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidConfiguration.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class InvalidConfiguration extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidName.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class InvalidName extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidRelation.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class InvalidRelation extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidType.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoConnection extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/NoEntity.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoEntity extends Exception
14 | {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/NoEntityManager.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoEntityManager extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/NoOperator.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NoOperator extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/NotJoined.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NotJoined extends Exception
14 | {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/NotScalar.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class NotScalar extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/UndefinedRelation.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class UndefinedRelation extends Exception
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/UnknownColumn.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class UnsupportedDriver extends Exception
14 | {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/MockTrait.php:
--------------------------------------------------------------------------------
1 | handlers[$event])) {
21 | $this->handlers[$event] = [];
22 | }
23 | $this->handlers[$event][] = $listener;
24 | return $this;
25 | }
26 |
27 | /**
28 | * Remove all listeners for $event
29 | *
30 | * @param $event
31 | * @return $this
32 | */
33 | public function off($event)
34 | {
35 | $this->handlers[$event] = [];
36 | return $this;
37 | }
38 |
39 | public function handle(Event $event)
40 | {
41 | $handlers = isset($this->handlers[$event::NAME]) ? $this->handlers[$event::NAME] : [];
42 | foreach ($handlers as $handler) {
43 | if (call_user_func($handler, $event) === false || $event->stopped) {
44 | return $event->stopped;
45 | }
46 | }
47 |
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/ObserverInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class Parenthesis implements ParenthesisInterface
12 | {
13 | use HasWhereConditions;
14 |
15 | /** Callback to close the parenthesis
16 | * @var callable */
17 | protected $onClose;
18 |
19 | /** Parent parenthesis or query
20 | * @var ParenthesisInterface */
21 | protected $parent;
22 |
23 | /**
24 | * Constructor
25 | *
26 | * Create a parenthesis inside another parenthesis or a query.
27 | *
28 | * @param callable $onClose Callable that gets executed when the parenthesis get closed
29 | * @param ParenthesisInterface $parent Parent where createWhereCondition get executed
30 | */
31 | public function __construct(
32 | callable $onClose,
33 | ParenthesisInterface $parent
34 | ) {
35 | $this->onClose = $onClose;
36 | $this->parent = $parent;
37 | }
38 |
39 | /** {@inheritdoc} */
40 | public function parenthesis()
41 | {
42 | return $this->andParenthesis();
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | * @internal
48 | */
49 | public function createWhereCondition($column, $operator = null, $value = null)
50 | {
51 | return $this->parent->createWhereCondition($column, $operator, $value);
52 | }
53 |
54 | /**
55 | * Build a where in expression
56 | *
57 | * Calls buildWhereInExpression() from parent if there is a parent.
58 | *
59 | * @param string|array $column Column or expression with placeholders
60 | * @param array $values Value (required when used with operator)
61 | * @param bool $inverse
62 | * @return string
63 | * @internal
64 | */
65 | public function buildWhereInExpression($column, array $values, $inverse = false)
66 | {
67 | return $this->parent->buildWhereInExpression($column, $values, $inverse);
68 | }
69 |
70 | /** {@inheritdoc} */
71 | public function andParenthesis()
72 | {
73 | return new Parenthesis(
74 | function (ParenthesisInterface $parenthesis) {
75 | $this->where[] = $this->wherePrefix('AND') . $parenthesis->getExpression();
76 |
77 | return $this;
78 | },
79 | $this
80 | );
81 | }
82 |
83 | /** {@inheritdoc} */
84 | public function orParenthesis()
85 | {
86 | return new Parenthesis(
87 | function (ParenthesisInterface $parenthesis) {
88 | $this->where[] = $this->wherePrefix('OR') . $parenthesis->getExpression();
89 |
90 | return $this;
91 | },
92 | $this
93 | );
94 | }
95 |
96 | /** {@inheritdoc} */
97 | public function close()
98 | {
99 | return call_user_func($this->onClose, $this);
100 | }
101 |
102 | /** {@inheritdoc} */
103 | public function getExpression()
104 | {
105 | return '(' . implode(' ', $this->where) . ')';
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Relation/HasOpponent.php:
--------------------------------------------------------------------------------
1 | class, 'getRelation' ], $this->opponent);
22 |
23 | if ($requiredType && !$opponent instanceof $requiredType) {
24 | throw new InvalidConfiguration(sprintf(
25 | "The opponent of a %s relation has to be a %s relation. Relation of type %s returned for relation %s" .
26 | " of entity %s",
27 | Helper::shortName(get_class($this)),
28 | Helper::shortName($requiredType),
29 | get_class($opponent),
30 | $this->name,
31 | $this->parent
32 | ));
33 | }
34 |
35 | return $opponent;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Relation/HasReference.php:
--------------------------------------------------------------------------------
1 | reference;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Relation/OneToOne.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class OneToOne extends OneToMany
16 | {
17 | /** {@inheritdoc} */
18 | public function fetch(Entity $self, EntityManager $entityManager)
19 | {
20 | return parent::fetch($self, $entityManager)->one();
21 | }
22 |
23 | /** {@inheritDoc} */
24 | public static function fromShort($parent, array $short)
25 | {
26 | // the cardinality is mandatory for one to one
27 | if ($short[0] !== self::CARDINALITY_ONE) {
28 | return null;
29 | }
30 | array_shift($short);
31 |
32 | return static::createStaticFromShort($short);
33 | }
34 |
35 | /** {@inheritDoc} */
36 | protected static function fromAssoc($parent, array $relDef)
37 | {
38 | if (!isset($relDef[self::OPT_CARDINALITY]) || $relDef[self::OPT_CARDINALITY] === self::CARDINALITY_MANY) {
39 | return null;
40 | }
41 |
42 | return self::createStaticFromAssoc($relDef);
43 | }
44 |
45 | /** {@inheritDoc} */
46 | public function eagerLoad(EntityManager $em, Entity ...$entities)
47 | {
48 | $foreignObjects = $this->getOpponent(Owner::class)->eagerLoadSelf($em, ...$entities);
49 | foreach ($entities as $entity) {
50 | $key = spl_object_hash($entity);
51 | $entity->setCurrentRelated(
52 | $this->name,
53 | isset($foreignObjects[$key]) ? Helper::first($foreignObjects[$key]) : null
54 | );
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Relation/ParentChildren.php:
--------------------------------------------------------------------------------
1 | '1.', children => [['path' => '1.2.'],['path' => '1.3.']]],
51 | * [path => '3.5.'],
52 | * [path => '4.'],
53 | * ]
54 | * ```
55 | *
56 | * Example usage when you are using materialized paths:
57 | * ```php
58 | * $children = $em->fetch('Category::class')->where('path', 'LIKE', '3.5._%')->all();
59 | * $treeOf35 = Category::getRelation('children')->buildTree(...$children);
60 | * ```
61 | *
62 | * @param Entity ...$entities
63 | * @return Entity[]
64 | */
65 | public function buildTree(Entity ...$entities)
66 | {
67 | $reference = $this->getOpponent(Owner::class)->getReference();
68 | /** @var Entity[] $entities */
69 | $entities = Helper::keyBy($entities, array_values($reference));
70 | $tree = [];
71 | foreach (Helper::groupBy($entities, array_keys($reference)) as $key => $children) {
72 | if (!isset($entities[$key])) {
73 | $tree = array_merge($tree, $children);
74 | continue;
75 | }
76 | $entities[$key]->setCurrentRelated($this->name, $children);
77 | }
78 | return $tree;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Testing/EntityFetcherMock.php:
--------------------------------------------------------------------------------
1 | currentResult === null) {
19 | $this->currentResult = $this->entityManager->getResults($this->class, $this);
20 | }
21 |
22 | return array_shift($this->currentResult);
23 | }
24 |
25 | /** {@inheritDoc} */
26 | public function count()
27 | {
28 | return count($this->entityManager->getResults($this->class, $this));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Testing/EntityManagerMock.php:
--------------------------------------------------------------------------------
1 | resultRepository = new ResultRepository($this);
23 | }
24 |
25 | /**
26 | * Add an entity to be fetched by primary key
27 | *
28 | * The entity needs to have a primary key if not it will be filled with random values between RANDOM_KEY_MIN and
29 | * RANDOM_KEY_MAX (at the time writing this it is 1000000000 and 1000999999).
30 | *
31 | * You can pass mocks from Entity too but we need to call `Entity::getPrimaryKey()`.
32 | *
33 | * @param Entity $entity
34 | * @codeCoverageIgnore proxy method
35 | */
36 | public function addEntity(Entity $entity)
37 | {
38 | $this->resultRepository->addEntity($entity);
39 | }
40 |
41 | /**
42 | * Retrieve an entity by $primaryKey
43 | *
44 | * @param string $class
45 | * @param array $primaryKey
46 | * @return Entity|null
47 | * @codeCoverageIgnore proxy method
48 | */
49 | public function retrieve($class, array $primaryKey)
50 | {
51 | return $this->resultRepository->retrieve($class, $primaryKey);
52 | }
53 |
54 | /**
55 | * Create and add a EntityFetcherMock\Result for $class
56 | *
57 | * As the results are mocked to come from the database they will also get a primary key if they don't have already.
58 | *
59 | * @param $class
60 | * @param Entity ...$entities
61 | * @return Result|m\MockInterface
62 | * @codeCoverageIgnore proxy method
63 | */
64 | public function addResult($class, Entity ...$entities)
65 | {
66 | return $this->resultRepository->addResult($class, ...$entities);
67 | }
68 |
69 | /**
70 | * Get the results for $class and $query
71 | *
72 | * The EntityFetcherMock\Result gets a quality for matching this query. Only the highest quality will be used.
73 | *
74 | * @param string $class
75 | * @param EntityFetcher $fetcher
76 | * @return array
77 | * @codeCoverageIgnore proxy method
78 | */
79 | public function getResults($class, EntityFetcher $fetcher)
80 | {
81 | return $this->resultRepository->getResults($class, $fetcher);
82 | }
83 |
84 | /** {@inheritDoc} */
85 | public function fetch($class, $primaryKey = null)
86 | {
87 | $reflection = new ReflectionClass($class);
88 | if (!$reflection->isSubclassOf(Entity::class)) {
89 | throw new NoEntity($class . ' is not a subclass of Entity');
90 | }
91 | /** @var string|Entity $class */
92 |
93 | if ($primaryKey === null) {
94 | return new EntityFetcherMock($this, $class);
95 | }
96 |
97 | $primaryKey = $this->buildPrimaryKey($class, (array)$primaryKey);
98 | $checksum = $this->buildChecksum($primaryKey);
99 |
100 | if (isset($this->map[$class][$checksum])) {
101 | return $this->map[$class][$checksum];
102 | }
103 |
104 | return $this->retrieve($class, $primaryKey);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/ClosureWrapper.php:
--------------------------------------------------------------------------------
1 | closure = $closure;
11 | }
12 | public function __invoke()
13 | {
14 | return call_user_func_array($this->closure, func_get_args());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/Constraint/ArraySubset.php:
--------------------------------------------------------------------------------
1 | strict = $strict;
20 | $this->subset = $subset;
21 | $this->delta = $delta;
22 | }
23 |
24 | protected function matches($other): bool
25 | {
26 | return count($this->arrayDiffRecursive($this->subset, $other)) === 0;
27 | }
28 |
29 | public function toString(): string
30 | {
31 | return 'has the subset ' . $this->exporter()->export($this->subset);
32 | }
33 |
34 | protected function failureDescription($other): string
35 | {
36 | return 'Failed asserting that an array has the expected subset. Differences:' . PHP_EOL .
37 | json_encode($this->arrayDiffRecursive($this->subset, $other), JSON_PRETTY_PRINT);
38 | }
39 |
40 | public function arrayDiffRecursive(
41 | array $expected,
42 | array $actual,
43 | array &$difference = [],
44 | string $parentPath = ''
45 | ): array {
46 | $oldKey = 'expected';
47 | $newKey = 'actual';
48 | foreach ($expected as $k => $v) {
49 | $path = ($parentPath ? $parentPath . '.' : '') . $k;
50 | if (is_array($v)) {
51 | if (!array_key_exists($k, $actual) || !is_array($actual[$k])) {
52 | $difference[$path][$oldKey] = $v;
53 | $difference[$path][$newKey] = null;
54 | } else {
55 | $this->arrayDiffRecursive($v, $actual[$k], $difference, $path);
56 | if (!empty($recursion)) {
57 | $difference[$oldKey][$k] = $recursion[$oldKey];
58 | $difference[$newKey][$k] = $recursion[$newKey];
59 | }
60 | }
61 | } else {
62 | if (!empty($v) && !array_key_exists($k, $actual)) {
63 | $difference[$path][$oldKey] = $v;
64 | $difference[$path][$newKey] = null;
65 | } else {
66 | $a = $actual[$k] ?? null;
67 | if ($this->delta && is_float($v)) {
68 | $diff = abs($a - $v);
69 | if ($diff > $this->delta) {
70 | $difference[$path][$oldKey] = $v;
71 | $difference[$path][$newKey] = $a;
72 | }
73 | } elseif ($this->strict && $a !== $v || $a != $v) {
74 | $difference[$path][$oldKey] = $v;
75 | $difference[$path][$newKey] = $a;
76 | }
77 | }
78 | }
79 | }
80 | return $difference;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/DbConfigTest.php:
--------------------------------------------------------------------------------
1 | newInstanceArgs($args);
47 |
48 | self::assertSame($expectedDsn, $dbConfig->getDsn());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Dbal/BasicTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Dbal\Other($this->em);
20 | }
21 |
22 | /** @test */
23 | public function escapesIdentifiersWithDoubleQuote()
24 | {
25 | $escaped = $this->dbal->escapeIdentifier('user');
26 |
27 | self::assertSame('"user"', $escaped);
28 | }
29 |
30 | /** @test */
31 | public function escapesSchemasInIdentifiers()
32 | {
33 | $escaped = $this->dbal->escapeIdentifier('user.id');
34 |
35 | self::assertSame('"user"."id"', $escaped);
36 | }
37 |
38 | /** @test */
39 | public function doesNotEscapeExpressions()
40 | {
41 | $escaped = $this->dbal->escapeIdentifier(EM::raw('DATE("column")'));
42 |
43 | self::assertSame('DATE("column")', $escaped);
44 | }
45 |
46 | /** @test */
47 | public function doesNotSupportDescribe()
48 | {
49 | self::expectException(Exception::class);
50 | self::expectExceptionMessage('Describe is not supported by this driver');
51 |
52 | $this->dbal->describe('any');
53 | }
54 |
55 |
56 |
57 | /** @test */
58 | public function doesNotSupportUpdateWithJoins()
59 | {
60 | self::expectException(Exception::class);
61 | self::expectExceptionMessage('Updates with joins are not supported by this driver');
62 |
63 | $this->em->query('examples')
64 | ->join('names', 'exampleId = examples.id')
65 | ->where('examples.id', 42)
66 | ->update(['foo' => EM::raw('names.foo')]);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Dbal/ColumnTest.php:
--------------------------------------------------------------------------------
1 | 'test_col',
15 | 'type' => Number::class,
16 | 'data_type' => 'tinyint',
17 | 'column_default' => '1',
18 | 'is_nullable' => 'YES',
19 | ];
20 |
21 | return [
22 | [$colDef, 'data_type', 'tinyint'],
23 | [$colDef, 'name', 'test_col'],
24 | [$colDef, 'default', '1'],
25 | [$colDef, 'nullable', true],
26 | ];
27 | }
28 |
29 | /** @dataProvider provideColumnDefinitions
30 | * @test */
31 | public function magicGetter($columnDefinition, $var, $expected)
32 | {
33 | $column = new Column($this->dbal, $columnDefinition);
34 |
35 | $result = $column->$var;
36 |
37 | self::assertSame($expected, $result);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Dbal/DataModificationTest.php:
--------------------------------------------------------------------------------
1 | pdo->shouldReceive('query')->with(
16 | "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo','bar')"
17 | )->once()->andReturn($statement = m::mock(PDOStatement::class));
18 | $statement->shouldReceive('rowCount')->andReturn(1);
19 |
20 | $this->dbal->insert('examples', ['col1' => 'foo', 'col2' => 'bar']);
21 | }
22 |
23 | /** @test */
24 | public function insertsEscapedValues()
25 | {
26 | $this->pdo->shouldReceive('query')->with(
27 | "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('hempel\\'s sofa','2020-11-12T08:42:00.000000Z')"
28 | )->once()->andReturn($statement = m::mock(PDOStatement::class));
29 | $statement->shouldReceive('rowCount')->andReturn(1);
30 |
31 | $this->dbal->insert('examples', ['col1' => 'hempel\'s sofa', 'col2' => new \DateTime('2020-11-12 08:42:00')]);
32 | }
33 |
34 | /** @test */
35 | public function insertsAllColumnsFromAllRows()
36 | {
37 | $this->pdo->shouldReceive('query')->with(
38 | "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo',NULL),(NULL,'bar')"
39 | )->once()->andReturn($statement = m::mock(PDOStatement::class));
40 | $statement->shouldReceive('rowCount')->andReturn(2);
41 |
42 | $this->dbal->insert('examples', ['col1' => 'foo'], ['col2' => 'bar']);
43 | }
44 |
45 | /** @test */
46 | public function insertReturns0IfNoRowsAreGiven()
47 | {
48 | $result = $this->dbal->insert('examples');
49 |
50 | self::assertSame(0, $result);
51 | }
52 |
53 | /** @test */
54 | public function insertReturnsTheNumberOfAffectedRows()
55 | {
56 | $this->pdo->shouldReceive('query')->with(
57 | "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo',NULL),(NULL,'bar')"
58 | )->once()->andReturn($statement = m::mock(PDOStatement::class));
59 | $statement->shouldReceive('rowCount')->andReturn(2);
60 |
61 | $result = $this->dbal->insert('examples', ['col1' => 'foo'], ['col2' => 'bar']);
62 |
63 | self::assertSame(2, $result);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Dbal/EscapeValueTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Dbal\Other($this->em);
21 | }
22 |
23 | /** @test */
24 | public function onlyConvertsScalarData()
25 | {
26 | $array = ['this','is','not','scalar'];
27 |
28 | self::expectException(NotScalar::class);
29 | self::expectExceptionMessage('$value has to be scalar data type. array given');
30 |
31 | $this->dbal->escapeValue($array);
32 | }
33 |
34 | public function provideScalars()
35 | {
36 | return [
37 | [42, '42'],
38 | [3E3, '3000'],
39 | [-5E-8, '-5.0E-8'],
40 | [0.002, '0.002'],
41 | [42.1, '42.1'],
42 | [null, 'NULL'],
43 | ];
44 | }
45 |
46 | /** @dataProvider provideScalars
47 | * @test */
48 | public function convertsScalar($value, $expected)
49 | {
50 | $result = $this->dbal->escapeValue($value);
51 |
52 | self::assertSame($expected, $result);
53 | }
54 |
55 | public function provideBooleanDefaults()
56 | {
57 | return [
58 | [true, '1'],
59 | [false, '0'],
60 | ];
61 | }
62 |
63 | /** @dataProvider provideBooleanDefaults
64 | * @test */
65 | public function booleanUseDefaults($value, $expected)
66 | {
67 | $result = $this->dbal->escapeValue($value);
68 |
69 | self::assertSame($expected, $result);
70 | }
71 |
72 | /** @test */
73 | public function doesNotConvertExpressions()
74 | {
75 | $result = $this->dbal->escapeValue(EM::raw('DATE(NOW())'));
76 |
77 | self::assertSame('DATE(NOW())', $result);
78 | }
79 |
80 | /** @test */
81 | public function dateTime()
82 | {
83 | $dateTime = \DateTime::createFromFormat('U.u', '1496163695.123456');
84 |
85 | $result = $this->dbal->escapeValue($dateTime);
86 |
87 | self::assertSame('\'2017-05-30T17:01:35.123456Z\'', $result);
88 | }
89 |
90 | /** @test */
91 | public function stringsUseQuote()
92 | {
93 | $this->pdo->shouldReceive('quote')->once()->with('foobar')->andReturn('\'buzzword\'');
94 |
95 | $result = $this->dbal->escapeValue('foobar');
96 |
97 | self::assertSame('\'buzzword\'', $result);
98 | }
99 |
100 | /** @test */
101 | public function numericString()
102 | {
103 | $result = $this->dbal->escapeValue('1.1234567890123456');
104 |
105 | self::assertSame('\'1.1234567890123456\'', $result);
106 | }
107 |
108 | /** @test */
109 | public function dateTimeDerivate()
110 | {
111 | $dateTime = DateTimeDerivate::createFromFormat('U.u', '1496163695.123456');
112 |
113 | $result = $this->dbal->escapeValue($dateTime);
114 |
115 | self::assertSame('\'2017-05-30T17:01:35.123456Z\'', $result);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/tests/Dbal/Mysql/UpdateJoinTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Mysql($this->em);
19 | }
20 |
21 | /** @test */
22 | public function executesAnUpdateJoinStatement()
23 | {
24 | $this->pdo->shouldReceive('query')->with(
25 | "UPDATE examples JOIN names ON exampleId = examples.id SET \"foo\" = names.foo WHERE examples.id = 42"
26 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
27 | $statement->shouldReceive('rowCount')->andReturn(1);
28 |
29 | $this->em->query('examples')
30 | ->join('names', 'exampleId = examples.id')
31 | ->where('examples.id', 42)
32 | ->update(['foo' => EM::raw('names.foo')]);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Dbal/Other/UpdateTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Other($this->em);
16 | }
17 |
18 | /** @test */
19 | public function executesAnUpdateStatement()
20 | {
21 | $this->pdo->shouldReceive('query')->with(
22 | "UPDATE \"examples\" SET \"foo\" = 'bar' WHERE \"id\" = 42"
23 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
24 | $statement->shouldReceive('rowCount')->andReturn(1);
25 |
26 | $this->dbal->update('examples', ['id' => 42], ['foo' => 'bar']);
27 | }
28 |
29 | /** @test */
30 | public function updatesEscapedValues()
31 | {
32 | $this->pdo->shouldReceive('query')->with(
33 | "UPDATE \"examples\" SET \"col1\" = 'hempel\\'s sofa',\"col2\" = 23 WHERE \"id\" = 42"
34 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
35 | $statement->shouldReceive('rowCount')->andReturn(1);
36 |
37 | $this->dbal->update('examples', ['id' => 42], ['col1' => 'hempel\'s sofa', 'col2' => 23]);
38 | }
39 |
40 | /** @test */
41 | public function doesNotNeedWhereConditions()
42 | {
43 | $this->pdo->shouldReceive('query')->with('UPDATE "questions" SET "answer" = 42')
44 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
45 | $statement->shouldReceive('rowCount')->andReturn(1);
46 |
47 | $this->dbal->update('questions', [], ['answer' => 42]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Dbal/Pgsql/UpdateFromTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Pgsql($this->em);
20 | }
21 |
22 | /** @test */
23 | public function executesAnUpdateFromStatement()
24 | {
25 | $this->pdo->shouldReceive('query')->with(
26 | "UPDATE examples SET \"foo\" = names.foo FROM names WHERE exampleId = examples.id AND examples.id = 42"
27 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
28 | $statement->shouldReceive('rowCount')->andReturn(1);
29 |
30 | $this->em->query('examples')
31 | ->join('names', 'exampleId = examples.id')
32 | ->where('examples.id', 42)
33 | ->update(['foo' => EM::raw('names.foo')]);
34 | }
35 |
36 | /** @test */
37 | public function updateFromOnlyWorksWithInnerJoins()
38 | {
39 | self::expectException(Exception::class);
40 | self::expectExceptionMessage('Only inner joins with on clause are allowed in update from statements');
41 |
42 | $this->em->query('examples')
43 | ->leftJoin('names', 'exampleId = examples.id')
44 | ->where('examples.id', 42)
45 | ->update(['foo' => EM::raw('names.foo')]);
46 | }
47 |
48 | /** @test */
49 | public function updateFromWorksAlsoWithoutJoins()
50 | {
51 | $this->pdo->shouldReceive('query')->with(
52 | "UPDATE examples SET \"foo\" = 'bar' WHERE id = 23"
53 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
54 | $statement->shouldReceive('rowCount')->andReturn(1);
55 |
56 | $this->em->query('examples')
57 | ->where('id', 23)
58 | ->update(['foo' => 'bar']);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/Dbal/Sqlite/InsertTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Sqlite($this->em);
20 | }
21 |
22 | /** @test */
23 | public function buildsValidQueryForCompositeKeys()
24 | {
25 | $entity = new ContactPhone(['id' => 1, 'name' => 'mobile', 'number' => '+1 555 123']);
26 |
27 | $this->pdo->shouldReceive('query')->with(m::pattern(
28 | '/INSERT INTO .* \("id","name","number"\) VALUES \(.*\)/'
29 | ))->once()->andReturn($statement = m::mock(\PDOStatement::class));
30 | $statement->shouldReceive('rowCount')->andReturn(1);
31 | $this->pdo->shouldReceive('query')->with(m::pattern(
32 | '/SELECT \* FROM .* WHERE \("id","name"\) IN \(VALUES (\(.*\))(,\(.*\))*\)/'
33 | ))->once()->andReturn($statement = m::mock(\PDOStatement::class));
34 | $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC)->andReturnTrue();
35 | $statement->shouldReceive('fetch')->with()
36 | ->times(2)->andReturn(
37 | ['id' => 1, 'name' => 'mobile', 'number' => '+1 555 123', 'created' => date('c')],
38 | false
39 | );
40 |
41 | $this->dbal->insertAndSync($entity);
42 |
43 | self::assertSame('+1 555 123', $entity->number);
44 | self::assertNotNull($entity->created);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Dbal/Sqlite/UpdateFromTest.php:
--------------------------------------------------------------------------------
1 | dbal = new Sqlite($this->em);
19 | }
20 |
21 | /** @test */
22 | public function executesAnUpdateFromStatement()
23 | {
24 | $this->pdo->shouldReceive('query')->with(
25 | "UPDATE examples SET \"foo\" = names.foo FROM names WHERE exampleId = examples.id AND examples.id = 42"
26 | )->once()->andReturn($statement = m::mock(\PDOStatement::class));
27 | $statement->shouldReceive('rowCount')->andReturn(1);
28 |
29 | $this->em->query('examples')
30 | ->join('names', 'exampleId = examples.id')
31 | ->where('examples.id', 42)
32 | ->update(['foo' => EM::raw('names.foo')]);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/BooleanTest.php:
--------------------------------------------------------------------------------
1 | dbal->setOption(EntityManager::OPT_BOOLEAN_TRUE, $true);
52 | $this->dbal->setOption(EntityManager::OPT_BOOLEAN_FALSE, $false);
53 | $column = new Column($this->dbal, [
54 | 'column_name' => 'abool',
55 | 'type' => Boolean::class,
56 | 'data_type' => 'tinyint',
57 | ]);
58 | $type = $column->getType();
59 |
60 | $result = $type->validate($value);
61 |
62 | if ($expected !== true) {
63 | self::assertInstanceOf(Error::class, $result);
64 | self::assertSame($expected, $result->getMessage());
65 | } else {
66 | self::assertTrue($result);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/Custom/CustomColumn.php:
--------------------------------------------------------------------------------
1 | setTimezone(new \DateTimeZone('UTC'));
21 |
22 | return [
23 | [$dt, true],
24 | [$dt->format('Y-m-d H:i:s'), true],
25 | [$dt->format('c'), true],
26 | [$dt->format('Y-m-d\TH:i:s.u\Z'), true],
27 |
28 | // valid but no time
29 | ['+01234-01-01', '+01234-01-01 is not a valid date or date time expression'],
30 | ['-01234-01-01', '-01234-01-01 is not a valid date or date time expression'],
31 |
32 | ['NOW()', 'NOW() is not a valid date or date time expression'],
33 | ['23rd of June \'84 5pm', '23rd of June \'84 5pm is not a valid date or date time expression'],
34 | [$dt->format('r'), $dt->format('r') . ' is not a valid date or date time expression'],
35 | ];
36 | }
37 |
38 | /** @dataProvider provideValuesWithTime
39 | * @test */
40 | public function validateWithTime($value, $expected)
41 | {
42 | $type = new DateTime(3, false);
43 |
44 | $result = $type->validate($value);
45 |
46 | if ($expected !== true) {
47 | self::assertInstanceOf(Error::class, $result);
48 | self::assertSame($expected, $result->getMessage());
49 | } else {
50 | self::assertTrue($result);
51 | }
52 | }
53 |
54 | public function provideValuesWithoutTime()
55 | {
56 | $dt = \DateTime::createFromFormat('U.u', microtime(true))
57 | ->setTimezone(new \DateTimeZone('UTC'));
58 |
59 | return [
60 | [$dt, true],
61 | ['+01234-01-01', true],
62 | ['-01234-01-01', true],
63 | ['1984-01-21', true],
64 |
65 | // valid but with time
66 | [$dt->format('Y-m-d H:i:s'), true],
67 | [$dt->format('c'), true],
68 | [$dt->format('Y-m-d\TH:i:s.u\Z'), true],
69 |
70 | ['NOW()', 'NOW() is not a valid date or date time expression'],
71 | ['23rd of June \'84 5pm', '23rd of June \'84 5pm is not a valid date or date time expression'],
72 | [$dt->format('r'), $dt->format('r') . ' is not a valid date or date time expression'],
73 | ];
74 | }
75 |
76 | /** @dataProvider provideValuesWithoutTime
77 | * @test */
78 | public function validateWithoutTime($value, $expected)
79 | {
80 | $type = new DateTime(3, true);
81 |
82 | $result = $type->validate($value);
83 |
84 | if ($expected !== true) {
85 | self::assertInstanceOf(Error::class, $result);
86 | self::assertSame($expected, $result->getMessage());
87 | } else {
88 | self::assertTrue($result);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/EnumTest.php:
--------------------------------------------------------------------------------
1 | validate($value);
35 |
36 | if ($expected !== true) {
37 | self::assertInstanceOf(Error::class, $result);
38 | self::assertSame($expected, $result->getMessage());
39 | } else {
40 | self::assertTrue($result);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/JsonTest.php:
--------------------------------------------------------------------------------
1 | 'value']), true],
21 | [json_encode(null), true],
22 | [json_encode(0), true],
23 | [json_encode(42), true],
24 | [json_encode(true), true],
25 | [json_encode(false), true],
26 | [json_encode(['a','b','c']), true],
27 |
28 | [42, 'Only string values are allowed for json'],
29 | // json allows only double quotes
30 | ['{\'key\':\'value\'}', '\'{\'key\':\'value\'}\' is not a valid JSON string'],
31 | // no valid json
32 | ['undefined', '\'undefined\' is not a valid JSON string'],
33 | ];
34 | }
35 |
36 | /** @dataProvider provideValues
37 | * @test */
38 | public function validate($value, $expected)
39 | {
40 | $type = new Json();
41 |
42 | $result = $type->validate($value);
43 |
44 | if ($expected !== true) {
45 | self::assertInstanceOf(Error::class, $result);
46 | self::assertSame($expected, $result->getMessage());
47 | } else {
48 | self::assertTrue($result);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/NumberTest.php:
--------------------------------------------------------------------------------
1 | validate($value);
41 |
42 | if ($expected !== true) {
43 | self::assertInstanceOf(Error::class, $result);
44 | self::assertSame($expected, $result->getMessage());
45 | } else {
46 | self::assertTrue($result);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/SetTest.php:
--------------------------------------------------------------------------------
1 | validate($value);
38 |
39 | if ($expected !== true) {
40 | self::assertInstanceOf(Error::class, $result);
41 | self::assertSame($expected, $result->getMessage());
42 | } else {
43 | self::assertTrue($result);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/TextTest.php:
--------------------------------------------------------------------------------
1 | validate($value);
34 |
35 | if ($expected !== true) {
36 | self::assertInstanceOf(Error::class, $result);
37 | self::assertSame($expected, $result->getMessage());
38 | } else {
39 | self::assertTrue($result);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/TimeTest.php:
--------------------------------------------------------------------------------
1 | setTimezone(new \DateTimeZone('UTC'));
21 |
22 | return [
23 | [$dt->format('H:i:s'), true],
24 | [$dt->format('H:i:s.u\Z'), true],
25 |
26 | ['NOW()', 'NOW() is not a valid time expression'],
27 | ['5pm', '5pm is not a valid time expression'],
28 | [$dt, 'DateTime is not allowed for time'],
29 | [$dt->format('c'), $dt->format('c') . ' is not a valid time expression'],
30 | [$dt->format('r'), $dt->format('r') . ' is not a valid time expression'],
31 | ];
32 | }
33 |
34 | /** @dataProvider provideValues
35 | * @test */
36 | public function validate($value, $expected)
37 | {
38 | $type = new Time(3);
39 |
40 | $result = $type->validate($value);
41 |
42 | if ($expected !== true) {
43 | self::assertInstanceOf(Error::class, $result);
44 | self::assertSame($expected, $result->getMessage());
45 | } else {
46 | self::assertTrue($result);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Dbal/Type/VarCharTest.php:
--------------------------------------------------------------------------------
1 | validate($value);
38 |
39 | if ($expected !== true) {
40 | self::assertInstanceOf(Error::class, $result);
41 | self::assertSame($expected, $result->getMessage());
42 | } else {
43 | self::assertTrue($result);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Entity/BasicTest.php:
--------------------------------------------------------------------------------
1 | shouldHaveBeenCalled()->once();
24 | }
25 |
26 | /** @test */
27 | public function entityGetsBootedOnlyOnce()
28 | {
29 | BootTestEntity::$bootSpy = $spy = \Mockery::spy(function () {
30 | });
31 |
32 | $bte = new BootTestEntity();
33 | $bte = new BootTestEntity();
34 |
35 | $spy->shouldHaveBeenCalled()->once();
36 | }
37 |
38 | /** @test */
39 | public function entityGetsBootedOnUnserialize()
40 | {
41 | $bteCache = serialize(new BootTestEntity());
42 | BootTestEntity::resetBooting();
43 |
44 | BootTestEntity::$bootSpy = $spy = \Mockery::spy(function () {
45 | });
46 |
47 | $bte = unserialize($bteCache);
48 |
49 | $spy->shouldHaveBeenCalled()->once();
50 | }
51 |
52 | /** @test */
53 | public function entityGetsBootedOnCreatingEntityFetcher()
54 | {
55 | BootTestEntity::$bootSpy = $spy = \Mockery::spy(function () {
56 | });
57 |
58 | $fetcher = BootTestEntity::query();
59 |
60 | $spy->shouldHaveBeenCalled()->once();
61 | }
62 |
63 | /** @test */
64 | public function traitGetsBooted()
65 | {
66 | BootTestEntity::$bootTestTraitSpy = $spy = \Mockery::spy(function () {
67 | });
68 |
69 | new BootTestEntity();
70 |
71 | $spy->shouldHaveBeenCalled()->once();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Article.php:
--------------------------------------------------------------------------------
1 | [Category::class, ['id' => 'article_id'], 'articles', 'article_category'],
22 | 'writer' => [User::class, ['userId' => 'id']],
23 | 'tags' => [Tag::class, 'parent'],
24 | 'tagsByClass' => [Tag::class, 'parentNoMap'],
25 | 'tagsOverArticleId' => [Tag::class, 'parentDifferentPk'],
26 | 'tagsByArticleId' => [Tag::class, 'parentDifferentFk'],
27 | ];
28 |
29 | public function getIntro()
30 | {
31 | return implode('', array_slice(preg_split(
32 | '/([\s,\.;\?\!]+)/',
33 | $this->text,
34 | 61,
35 | PREG_SPLIT_DELIM_CAPTURE
36 | ), 0, 59));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Category.php:
--------------------------------------------------------------------------------
1 | [Article::class, ['id' => 'category_id'], 'categories', 'article_category'],
14 | 'articlesAssoc' => [
15 | Relation::OPT_CARDINALITY => Relation::CARDINALITY_MANY,
16 | Relation::OPT_CLASS => Article::class,
17 | Relation::OPT_REFERENCE => ['id' => 'category_id'],
18 | Relation::OPT_OPPONENT => 'categories',
19 | Relation::OPT_TABLE => 'article_category',
20 | ],
21 | 'parent' => [self::class, ['parentId' => 'id']],
22 | 'children' => [self::class, 'parent'],
23 | ];
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Concerns/WithCreated.php:
--------------------------------------------------------------------------------
1 | observe(static::class)
15 | ->on('inserting', function (Event $event) {
16 | $now = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.u\Z');
17 | $event->entity->setAttribute('updated', $now);
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Concerns/WithTimestamps.php:
--------------------------------------------------------------------------------
1 | observe(static::class)
15 | ->on('inserting', function (Event $event) {
16 | $now = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.u\Z');
17 | $event->entity->setAttribute('updated', $now);
18 | })
19 | ->on('updating', function (Event $event) {
20 | $now = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.u\Z');
21 | $event->entity->setAttribute('updated', $now);
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/ContactPhone.php:
--------------------------------------------------------------------------------
1 | [RelationExample::class, ['relationId' => 'id']],
13 | ];
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/DamagedABBRVCase.php:
--------------------------------------------------------------------------------
1 | ['one', RelationExample::class, 'dmgd'],
11 | 'undefined1t1' => ['one', StudlyCaps::class, 'dmgd'],
12 | 'undefined1tm' => [StudlyCaps::class, 'dmgd']
13 | ];
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/GeneratesUuid.php:
--------------------------------------------------------------------------------
1 | id = uniqid();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Image.php:
--------------------------------------------------------------------------------
1 | [Tag::class, 'parent'],
11 | ];
12 | }
13 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Psr0_StudlyCaps.php:
--------------------------------------------------------------------------------
1 | [
12 | Relation::OPT_CARDINALITY => 'many',
13 | Relation::OPT_CLASS => StudlyCaps::class,
14 | Relation::OPT_REFERENCE => ['studlyCapsId' => 'id'],
15 | ],
16 | 'psr0StudlyCaps' => [
17 | Relation::OPT_CLASS => Psr0_StudlyCaps::class,
18 | Relation::OPT_REFERENCE => ['psr0StudlyCaps' => 'id'],
19 | ],
20 | 'contactPhones' => [
21 | Relation::OPT_CLASS => ContactPhone::class,
22 | Relation::OPT_OPPONENT => 'relation',
23 | ],
24 | 'dmgd' => [DamagedABBRVCase::class, ['dmgdId' => 'id']],
25 | 'invalid' => [StudlyCaps::class, 'opponent', 'something'], // additional data is not allowed
26 | 'mySnake' => ['one', Snake_Ucfirst::class, 'relation'],
27 | 'mySnakeAssoc' => [
28 | Relation::OPT_CARDINALITY => Relation::CARDINALITY_ONE,
29 | Relation::OPT_CLASS => Snake_Ucfirst::class,
30 | Relation::OPT_OPPONENT => 'relation',
31 | ],
32 | 'snake' => [Snake_Ucfirst::class, ['snakeId' => 'id']]
33 | ];
34 |
35 | protected static function anotherSnakeRelation()
36 | {
37 | return new Relation\OneToOne(Snake_Ucfirst::class, 'relation');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Snake_Ucfirst.php:
--------------------------------------------------------------------------------
1 | 'another_var'
14 | ];
15 |
16 | protected static $relations = [
17 | 'relations' => [RelationExample::class, 'snake'],
18 | 'relation' => ['one', RelationExample::class, 'mySnake'],
19 | 'invalid' => [RelationExample::class, 'mySnake']
20 | ];
21 |
22 | public function set_another_var($value)
23 | {
24 | }
25 |
26 | public function get_another_var()
27 | {
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/StaticTableName.php:
--------------------------------------------------------------------------------
1 | 'bar'
15 | ];
16 |
17 | protected $data = [
18 | 'bar' => 'default'
19 | ];
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/StudlyCaps.php:
--------------------------------------------------------------------------------
1 | anotherVar = $value;
21 | }
22 |
23 | public function getAnotherVar()
24 | {
25 | return $this->anotherVar;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Tag.php:
--------------------------------------------------------------------------------
1 | [['parentType' => [
11 | 'image' => Image::class,
12 | 'article' => Article::class,
13 | ]], ['parentId']],
14 | 'parentNoMap' => [['parentType' => Taggable::class], ['parentId' => 'id']],
15 | 'parentDifferentPk' => [
16 | ['parentType' => [
17 | 'image' => Image::class,
18 | 'article' => Article::class,
19 | ]],
20 | ['parentId' => [
21 | 'image' => 'imageId',
22 | 'article' => 'articleId',
23 | ]],
24 | ],
25 | 'parentDifferentFk' => [
26 | ['parentType' => [
27 | 'image' => Image::class,
28 | 'article' => Article::class,
29 | ]],
30 | [
31 | 'image' => ['imageId' => 'id'],
32 | 'article' => ['articleId' => 'id'],
33 | ],
34 | ],
35 | ];
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/Taggable.php:
--------------------------------------------------------------------------------
1 | [Article::class, 'writer'],
11 | 'contact' => ['one', UserContact::class, 'user'],
12 | ];
13 | }
14 |
--------------------------------------------------------------------------------
/tests/Entity/Examples/UserContact.php:
--------------------------------------------------------------------------------
1 | [User::class, ['userId' => 'id']],
11 | ];
12 | }
13 |
--------------------------------------------------------------------------------
/tests/Entity/ExistsTest.php:
--------------------------------------------------------------------------------
1 | 42, 'title' => 'Foobar'], $this->em, true);
15 |
16 | self::assertTrue($entity->exists());
17 | }
18 |
19 | /** @test */
20 | public function afterInsertAnEntityExists()
21 | {
22 | $entity = new Article();
23 |
24 | $this->em->shouldReceive('insert')->with($entity, true)
25 | ->once()->andReturn(true);
26 |
27 | $entity->save();
28 |
29 | self::assertTrue($entity->exists());
30 | }
31 |
32 | /** @test */
33 | public function afterSyncAnEntityExists()
34 | {
35 | $entity = new Article(['id' => 42]);
36 |
37 | $this->pdo->shouldReceive('query')->with('SELECT DISTINCT t0.* FROM "article" AS t0 WHERE "t0"."id" = 42')
38 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
39 | $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC)
40 | ->once()->andReturn(['id' => 42, 'title' => 'Foobar']);
41 |
42 | $this->em->sync($entity);
43 |
44 | self::assertTrue($entity->exists());
45 | }
46 |
47 | /** @test */
48 | public function afterRestoreAnEntityStillExists()
49 | {
50 | $serialized = serialize(new Article(['id' => 42], $this->em, true));
51 |
52 | $entity = unserialize($serialized);
53 |
54 | self::assertTrue($entity->exists());
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/Entity/HelperTest.php:
--------------------------------------------------------------------------------
1 | fetch(Article::class), $fetcher);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Entity/IssetTest.php:
--------------------------------------------------------------------------------
1 | makePartial();
14 |
15 | $entity->shouldReceive('getIntro')->once()->andReturn('Any text');
16 |
17 | $result = isset($entity->intro);
18 |
19 | self::assertTrue($result);
20 | }
21 |
22 | /** @test */
23 | public function loadsTheRelation()
24 | {
25 | $entity = \Mockery::mock(Article::class)->makePartial();
26 |
27 | $entity->shouldReceive('getRelated')->with('categories')->andReturn([]);
28 |
29 | $result = isset($entity->categories);
30 |
31 | self::assertFalse($result);
32 | }
33 |
34 | /** @test */
35 | public function checksIfValueExists()
36 | {
37 | $entity = new Article(['published' => false]);
38 |
39 | $result = isset($entity->published);
40 |
41 | self::assertTrue($result);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Entity/ValidateTest.php:
--------------------------------------------------------------------------------
1 | mocks['table'] = \Mockery::mock(Table::class);
24 | $this->mocks['em']->shouldReceive('describe')->with('article')->andReturn($this->mocks['table'])->byDefault();
25 | }
26 |
27 | /** @test */
28 | public function getsTableFromEmDescribe()
29 | {
30 | $this->mocks['em']->shouldReceive('describe')->with('article')->once()->andReturn($this->mocks['table']);
31 |
32 | Article::describe();
33 | }
34 |
35 | /** @test */
36 | public function validateUsesTableFromDescribe()
37 | {
38 | $this->mocks['table']->shouldReceive('validate')->with('title', 'Hello World!')->once()->andReturn(true);
39 |
40 | Article::validate('title', 'Hello World!');
41 | }
42 |
43 | /** @test */
44 | public function convertsFieldNamesToColumns()
45 | {
46 | $this->mocks['table']->shouldReceive('validate')->with('intro_text', 'This is just a test article.')
47 | ->once()->andReturn(true);
48 |
49 | Article::validate('introText', 'This is just a test article.');
50 | }
51 |
52 | /** @test */
53 | public function validateArray()
54 | {
55 | $this->mocks['table']->shouldReceive('validate')->with('title', 'Hello World!')->once()->andReturn(true);
56 | $this->mocks['table']->shouldReceive('validate')->with('intro_text', 'This is just a test article.')
57 | ->once()->andReturn(true);
58 |
59 | $result = Article::validateArray([
60 | 'title' => 'Hello World!',
61 | 'introText' => 'This is just a test article.',
62 | ]);
63 |
64 | self::assertSame(['title' => true, 'introText' => true], $result);
65 | }
66 |
67 | /** @test */
68 | public function byDefaultValidatorIsDisabled()
69 | {
70 | self::assertFalse(Article::isValidatorEnabled());
71 | }
72 |
73 | /**
74 | * @depends testByDefaultValidatorIsDisabled
75 | */
76 | /** @test */
77 | public function validatorCanBeEnabled()
78 | {
79 | Article::enableValidator();
80 |
81 | self::assertTrue(Article::isValidatorEnabled());
82 | }
83 |
84 | /**
85 | * @depends testValidatorCanBeEnabled
86 | */
87 | /** @test */
88 | public function validatorCanBeDisabled()
89 | {
90 | Article::disableValidator();
91 |
92 | self::assertFalse(Article::isValidatorEnabled());
93 | }
94 |
95 | /** @test */
96 | public function validatorCanBeActivatedByStatic()
97 | {
98 | self::assertTrue(Category::isValidatorEnabled());
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/EntityFetcher/CountTest.php:
--------------------------------------------------------------------------------
1 | statement = m::mock(\PDOStatement::class);
22 |
23 | $this->pdo->shouldReceive('query')->with(m::pattern('/^SELECT COUNT\(\*\) FROM \(.*\) AS stmt/'))
24 | ->andReturn($this->statement)->byDefault();
25 | $this->statement->shouldReceive('fetchColumn')->with()
26 | ->andReturn(42)->byDefault();
27 | }
28 |
29 | /** @test */
30 | public function countReturnsInteger()
31 | {
32 | $fetcher = $this->em->fetch(DamagedABBRVCase::class);
33 |
34 | $result = $fetcher->count();
35 |
36 | self::assertIsInt($result);
37 | }
38 |
39 | /** @test */
40 | public function countExecutesQuery()
41 | {
42 | /** @var EntityFetcher $fetcher */
43 | $fetcher = $this->em->fetch(StudlyCaps::class);
44 |
45 | $this->pdo->shouldReceive('query')
46 | ->with('SELECT COUNT(*) FROM (SELECT DISTINCT t0.* FROM "studly_caps" AS t0) AS stmt')
47 | ->once()->andReturn($this->statement);
48 | $this->statement->shouldReceive('fetchColumn')->with()
49 | ->once()->andReturn(42);
50 |
51 | $fetcher->count();
52 | }
53 |
54 | /** @test */
55 | public function fetchesAfterCount()
56 | {
57 | /** @var EntityFetcher $fetcher */
58 | $fetcher = $this->em->fetch(StudlyCaps::class);
59 | // we change column and modifier here
60 | $fetcher->count();
61 |
62 | // if we not reset it will execute a count query again and the count query will not fail
63 | self::expectException(\PDOException::class);
64 | self::expectExceptionMessage('Query failed by default');
65 |
66 | $fetcher->one();
67 | }
68 |
69 | /** @test */
70 | public function convertsResultToInt()
71 | {
72 | /** @var EntityFetcher $fetcher */
73 | $fetcher = $this->em->fetch(StudlyCaps::class);
74 | $this->statement->shouldReceive('fetchColumn')->with()
75 | ->once()->andReturn('42');
76 |
77 | $result = $fetcher->count();
78 |
79 | self::assertSame(42, $result);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/EntityFetcher/EagerLoadTest.php:
--------------------------------------------------------------------------------
1 | em, User::class])->makePartial();
17 | $fetcher->shouldReceive('one')->times(3)->andReturn(
18 | $user1 = new User(['id' => 1]),
19 | $user2 = new User(['id' => 2]),
20 | null
21 | );
22 |
23 | $this->em->shouldReceive('eagerLoad')->with('articles.categories', $user1, $user2)->once();
24 |
25 | $fetcher->with('articles.categories');
26 | $fetcher->all();
27 | }
28 |
29 | /** @test */
30 | public function eagerLoadsAllDefinedRelations()
31 | {
32 | /** @var EntityFetcher|m\MockInterface $fetcher */
33 | $fetcher = m::mock(EntityFetcher::class, [$this->em, User::class])->makePartial();
34 | $fetcher->shouldReceive('one')->times(3)->andReturn(
35 | $user1 = new User(['id' => 1]),
36 | $user2 = new User(['id' => 2]),
37 | null
38 | );
39 |
40 | $this->em->shouldReceive('eagerLoad')->with('articles', $user1, $user2)->once()->ordered();
41 | $this->em->shouldReceive('eagerLoad')->with('articles.categories', $user1, $user2)->once()->ordered();
42 | $this->em->shouldReceive('eagerLoad')->with('contact', $user1, $user2)->once()->ordered();
43 |
44 | $fetcher->with('articles', 'articles.categories');
45 | $fetcher->with('contact')->all();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/EntityFetcher/Examples/NotDeletedFilter.php:
--------------------------------------------------------------------------------
1 | where('deleted IS NULL');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/EntityFetcher/ExecutesQueriesTest.php:
--------------------------------------------------------------------------------
1 | em->fetch(Article::class)->where('someColumn', 42);
15 |
16 | $this->pdo->shouldReceive('query')
17 | ->with('UPDATE "article" AS t0 SET "any_column" = \'foo bar\' WHERE "t0"."some_column" = 42')
18 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
19 | $statement->shouldReceive('rowCount')->once()->andReturn(3);
20 |
21 | $fetcher->update(['anyColumn' => 'foo bar']);
22 | }
23 |
24 | /** @test */
25 | public function updateUsesAllWhereConditions()
26 | {
27 | $fetcher = $this->em->fetch(Article::class)->where('col1', 42)->where('col2', 23);
28 |
29 | $this->pdo->shouldReceive('query')
30 | ->with('UPDATE "article" AS t0 SET "col3" = \'foo bar\' WHERE "t0"."col1" = 42 AND "t0"."col2" = 23')
31 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
32 | $statement->shouldReceive('rowCount')->once()->andReturn(3);
33 |
34 | $fetcher->update(['col3' => 'foo bar']);
35 | }
36 |
37 | /** @test */
38 | public function insertTranslatesColumnNames()
39 | {
40 | $fetcher = $this->em->fetch(Article::class);
41 |
42 | $this->pdo->shouldReceive('query')
43 | ->with('INSERT INTO "article" ("first_column","second_column") VALUES (\'foo bar\',NULL),(NULL,42)')
44 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
45 | $statement->shouldReceive('rowCount')->once()->andReturn(1);
46 |
47 | $fetcher->insert(['firstColumn' => 'foo bar'], ['secondColumn' => 42]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/EntityManager/BulkInsertTest.php:
--------------------------------------------------------------------------------
1 | mocks['bulkInsert'] = $this->bulkInsert = m::mock(BulkInsert::class, [$this->dbal, Article::class])
21 | ->makePartial();
22 | $this->em->setBulkInsert(Article::class, $this->bulkInsert);
23 | }
24 |
25 | /** @test */
26 | public function createsABulkInsert()
27 | {
28 | $onSync = function ($synced) {
29 | };
30 |
31 | $bulkInsert = $this->em->useBulkInserts(Category::class, false, 30, $onSync);
32 |
33 | self::assertEquals(new BulkInsert($this->dbal, Category::class, false, 30, $onSync), $bulkInsert);
34 | }
35 |
36 | /** @test */
37 | public function returnsExistingBulkInsert()
38 | {
39 | $bulkInsert = new BulkInsert($this->dbal, Article::class);
40 | $this->em->setBulkInsert(Article::class, $bulkInsert);
41 |
42 | $result = $this->em->useBulkInserts(Article::class, false, 30);
43 |
44 | self::assertSame($bulkInsert, $result);
45 | }
46 |
47 | /** @test */
48 | public function insertAddsToBulkInsert()
49 | {
50 | $entity = new Article;
51 | $this->bulkInsert->shouldReceive('add')->with($entity)
52 | ->once();
53 |
54 | $this->em->insert($entity);
55 | }
56 |
57 | /** @test */
58 | public function insertOtherEntitiesNot()
59 | {
60 | $entity = new Category;
61 | $this->bulkInsert->shouldNotReceive('add');
62 | $this->dbal->shouldReceive('insertAndSyncWithAutoInc')->with($entity)
63 | ->once()->andReturn($entity);
64 |
65 | $this->em->insert($entity);
66 | }
67 |
68 | /** @test */
69 | public function finishesBulkImport()
70 | {
71 | $synced = [new Article];
72 | $this->bulkInsert->shouldReceive('finish')->with()
73 | ->once()->andReturn($synced);
74 |
75 | $result = $this->em->finishBulkInserts(Article::class);
76 |
77 | self::assertSame($synced, $result);
78 | }
79 |
80 | /** @test */
81 | public function removesTheBulkImport()
82 | {
83 | $this->bulkInsert->shouldReceive('finish')->with()
84 | ->once()->andReturn([]);
85 |
86 | $this->em->finishBulkInserts(Article::class);
87 |
88 | $entity = new Article;
89 | $this->bulkInsert->shouldNotReceive('add');
90 | $this->dbal->shouldReceive('insertAndSyncWithAutoInc')->with($entity)
91 | ->once()->andReturn($entity);
92 |
93 | $this->em->insert($entity);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/EntityManager/DescribeTest.php:
--------------------------------------------------------------------------------
1 | table = new Table([new Column($this->dbal, [
19 | 'column_name' => 'id',
20 | 'data_type' => 'int',
21 | 'column_default' => 'sequence(AUTO_INCREMENT)',
22 | 'is_nullable' => false
23 | ])]);
24 | }
25 |
26 | /** @test */
27 | public function callsDescribeFromDbal()
28 | {
29 | $this->dbal->shouldReceive('describe')->with('db.table')->once()->andReturn($this->table);
30 |
31 | $description = $this->em->describe('db.table');
32 |
33 | self::assertSame($this->table, $description);
34 | }
35 |
36 | /** @test */
37 | public function remembersPreviousCalls()
38 | {
39 | $this->dbal->shouldReceive('describe')->with('db.table')->once()->andReturn($this->table);
40 | $this->em->describe('db.table');
41 |
42 | $description = $this->em->describe('db.table');
43 |
44 | self::assertSame($this->table, $description);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/EntityManager/Examples/Concrete.php:
--------------------------------------------------------------------------------
1 | defineForNamespace(Examples\SubNamespace::class);
47 |
48 | self::assertSame($em, EM::getInstance(Examples\SubNamespace\Entity::class));
49 | }
50 |
51 | /** @test */
52 | public function defineForParent()
53 | {
54 | $em = new EM();
55 |
56 | $em->defineForParent(Entity::class);
57 |
58 | self::assertSame($em, EM::getInstance(Concrete::class));
59 | }
60 |
61 | /** @test */
62 | public function returnsLastIfNotSpecified()
63 | {
64 | $em = new EM();
65 | $em->defineForNamespace(Examples\SubNamespace::class);
66 | $em = new EM();
67 | $em->defineForParent(Entity::class);
68 | $last = new EM();
69 |
70 | self::assertSame($last, EM::getInstance(Unspecified::class));
71 | }
72 |
73 | /** @test */
74 | public function usesTheResolverProvided()
75 | {
76 | $em = new EM();
77 | $spy = m::spy(function ($class = null) use ($em) {
78 | return $em;
79 | });
80 | EM::setResolver($spy);
81 |
82 | $spy->shouldReceive('__invoke')->with(Concrete::class)->once()->andReturn($em);
83 |
84 | self::assertSame($em, EM::getInstance(Concrete::class));
85 | }
86 |
87 | /** @test */
88 | public function registeringNameSpaceHasNoEffectAfterReceivingInstance()
89 | {
90 | $em1 = new EM();
91 | EM::getInstance(Examples\Concrete::class);
92 |
93 | $em2 = new EM();
94 | $em2->defineForNamespace(Examples::class);
95 |
96 | self::assertSame($em1, EM::getInstance(Examples\Concrete::class));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/Examples/AuditObserver.php:
--------------------------------------------------------------------------------
1 | writeLog($event);
15 | }
16 |
17 | public function inserted(Event\Inserted $event)
18 | {
19 | $this->writeLog($event, $event->entity->toArray());
20 | }
21 |
22 | public function updated(Event\Updated $event)
23 | {
24 | $this->writeLog($event, $event->dirty);
25 | }
26 |
27 | public function deleted(Event\Deleted $event)
28 | {
29 | $this->writeLog($event);
30 | }
31 |
32 | protected function writeLog(Event $event, array $data = null)
33 | {
34 | $class = get_class($event->entity);
35 |
36 | if (!isset($this->log[$class])) {
37 | $this->log[$class] = [];
38 | }
39 |
40 | $this->log[$class][] = [
41 | 'action' => $event::NAME,
42 | 'key' => $event->entity->hasPrimaryKey() ? $event->entity->getPrimaryKey() : null,
43 | 'data' => $data,
44 | ];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Examples/CustomEvent.php:
--------------------------------------------------------------------------------
1 | format('Y-m-d H:i:s.u'), $dt->getTimezone());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/ExceptionsTest.php:
--------------------------------------------------------------------------------
1 | makePartial();
18 | $event = new Fetched(new Article(), ['id' => 1, 'title' => 'Foo Bar']);
19 |
20 | $observer->shouldReceive('fetched')->once()->andReturnFalse();
21 |
22 | $result = $observer->handle($event);
23 |
24 | self::assertFalse($result);
25 | }
26 |
27 | /** @test */
28 | public function returnsTrueWhenNoMethodIsDefined()
29 | {
30 | $observer = new AuditObserver();
31 | $event = new Saving(new Article());
32 |
33 | $result = $observer->handle($event);
34 |
35 | self::assertTrue($result);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Observer/FireEventTest.php:
--------------------------------------------------------------------------------
1 | 'Foo']);
17 |
18 | $result = $this->em->fire($event);
19 |
20 | self::assertTrue($result);
21 | }
22 |
23 | /** @test */
24 | public function returnsTrueWhenTheObserverReturnsNull()
25 | {
26 | $event = new Event\Fetched(new Article(), ['title' => 'Foo']);
27 | $observer = m::mock(AbstractObserver::class);
28 | $this->em->observe(Article::class, $observer);
29 |
30 | $observer->shouldReceive('handle')->once()->andReturnNull();
31 |
32 | $result = $this->em->fire($event);
33 |
34 | self::assertTrue($result);
35 | }
36 |
37 | /** @test */
38 | public function returnsFalseWhenTheObserverReturnsFalse()
39 | {
40 | $event = new Event\Fetched(new Article(), ['title' => 'Foo']);
41 | $observer = m::mock(AbstractObserver::class);
42 | $this->em->observe(Article::class, $observer);
43 |
44 | $observer->shouldReceive('handle')->once()->andReturnFalse();
45 |
46 | $result = $this->em->fire($event);
47 |
48 | self::assertFalse($result);
49 | }
50 |
51 | /** @test */
52 | public function stopsWhenAnObserverReturnsFalse()
53 | {
54 | $event = new Event\Fetched(new Article(), ['title' => 'Foo']);
55 | $observer = m::mock(AbstractObserver::class);
56 | $this->em->observe(Article::class, $observer);
57 | $observer2 = m::mock(AbstractObserver::class);
58 | $this->em->observe(Article::class, $observer2);
59 |
60 | $observer->shouldReceive('handle')->once()->andReturnFalse();
61 | $observer2->shouldNotReceive('handle');
62 |
63 | $this->em->fire($event);
64 | }
65 |
66 | /** @test */
67 | public function stopsWhenAnObserverStopsTheEvent()
68 | {
69 | $event = new Event\Fetched(new Article(), ['title' => 'Foo']);
70 | $observer = m::mock(AbstractObserver::class);
71 | $this->em->observe(Article::class, $observer);
72 | $observer2 = m::mock(AbstractObserver::class);
73 | $this->em->observe(Article::class, $observer2);
74 |
75 | $observer->shouldReceive('handle')->once()->andReturnUsing(function (Event\Fetched $event) {
76 | $event->stop();
77 | });
78 | $observer2->shouldNotReceive('handle');
79 |
80 | $this->em->fire($event);
81 | }
82 |
83 | /** @test */
84 | public function doesNotCauseANoticeWhenNoObserverIsDefined()
85 | {
86 | $spy = m::spy(function ($error) {
87 | });
88 | set_error_handler($spy);
89 | $spy->shouldNotReceive('__invoke');
90 |
91 | $event = new Event\Fetched(new Article(), ['title' => 'Foo']);
92 | $this->em->fire($event);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/QueryBuilder/FetchTest.php:
--------------------------------------------------------------------------------
1 | em->query('table');
14 | $row = ['id' => 42, 'name' => 'foobar'];
15 |
16 | $this->pdo->shouldReceive('query')->with('SELECT * FROM table')
17 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
18 | $statement->shouldReceive('fetch')->with()->once()->andReturn($row);
19 |
20 | $result = $query->one();
21 |
22 | self::assertSame($row, $result);
23 | }
24 |
25 | /** @test */
26 | public function returnsAllRows()
27 | {
28 | $query = $this->em->query('table');
29 | $row1 = ['id' => 42, 'name' => 'foobar'];
30 | $row2 = ['id' => 23, 'name' => 'foo bar'];
31 |
32 | $this->pdo->shouldReceive('query')->with('SELECT * FROM table')
33 | ->once()->andReturn($statement = m::mock(\PDOStatement::class));
34 | $statement->shouldReceive('fetch')->with()->times(3)
35 | ->andReturn($row1, $row2, false);
36 |
37 | $result = $query->all();
38 |
39 | self::assertSame([$row1, $row2], $result);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/QueryBuilder/JoinTest.php:
--------------------------------------------------------------------------------
1 | getQuery());
30 | self::assertSame($query, $result);
31 | }
32 |
33 | /** @dataProvider provideJoins
34 | * @test */
35 | public function joinWithExpression($method, $sql)
36 | {
37 | $query = new QueryBuilder('foo');
38 |
39 | $result = call_user_func([$query, $method], 'bar', 'bar_id = bar.id');
40 |
41 | self::assertSame('SELECT * FROM foo ' . $sql . ' bar ON bar_id = bar.id', $query->getQuery());
42 | self::assertSame($query, $result);
43 | }
44 |
45 | /** @dataProvider provideJoins
46 | * @test */
47 | public function joinWithExpressionAndArg($method, $sql)
48 | {
49 | $query = new QueryBuilder('foo');
50 |
51 | $result = call_user_func([$query, $method], 'bar', 'bar_id = bar.id AND bar.type = ?', '', 'pub');
52 |
53 | self::assertSame(
54 | 'SELECT * FROM foo ' . $sql . ' bar ON bar_id = bar.id AND bar.type = \'pub\'',
55 | $query->getQuery()
56 | );
57 | self::assertSame($query, $result);
58 | }
59 |
60 | /** @dataProvider provideJoins
61 | * @test */
62 | public function joinWithExpressionAndArgs($method, $sql)
63 | {
64 | $query = new QueryBuilder('foo');
65 |
66 | $result = call_user_func([$query, $method], 'bar', 'bar.id = ? AND bar.type = ?', '', [42, 'pub']);
67 |
68 | self::assertSame(
69 | 'SELECT * FROM foo ' . $sql . ' bar ON bar.id = 42 AND bar.type = \'pub\'',
70 | $query->getQuery()
71 | );
72 | self::assertSame($query, $result);
73 | }
74 |
75 | /** @dataProvider provideJoins
76 | * @test */
77 | public function joinWithParenthesis($method, $sql)
78 | {
79 | $query = new QueryBuilder('foo');
80 |
81 | /** @var Parenthesis $parenthesis */
82 | $parenthesis = call_user_func([$query, $method], 'bar', false);
83 | self::assertSame(Parenthesis::class, get_class($parenthesis));
84 |
85 | $parenthesis->where('bar_id = bar.id');
86 | $result = $parenthesis->close();
87 |
88 | self::assertSame('SELECT * FROM foo ' . $sql . ' bar ON (bar_id = bar.id)', $query->getQuery());
89 | self::assertSame($query, $result);
90 | }
91 |
92 | /** @dataProvider provideJoins
93 | * @test */
94 | public function emptyJoin($method, $sql)
95 | {
96 | $query = new QueryBuilder('foo');
97 |
98 | /** @var Parenthesis $parenthesis */
99 | $result = call_user_func([$query, $method], 'bar', true);
100 |
101 |
102 | self::assertSame('SELECT * FROM foo ' . $sql . ' bar', $query->getQuery());
103 | self::assertSame($query, $result);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Relation/ParentChildrenTest.php:
--------------------------------------------------------------------------------
1 | 1, 'parent_id' => null, 'name' => 'Category 1']),
25 | $category2 = new Category(['id' => 2, 'parent_id' => null, 'name' => 'Category 2']),
26 | $category11 = new Category(['id' => 3, 'parent_id' => 1, 'name' => 'Category 1.1']),
27 | $category12 = new Category(['id' => 4, 'parent_id' => 1, 'name' => 'Category 1.2']),
28 | $category111 = new Category(['id' => 5, 'parent_id' => 3, 'name' => 'Category 1.1.1']),
29 | ];
30 |
31 | $this->em->shouldNotReceive('fetch');
32 |
33 | $tree = Category::getRelation('children')->buildTree(...$entities);
34 |
35 | self::assertSame([$category1, $category2], $tree);
36 | self::assertSame([$category11, $category12], $category1->children);
37 | self::assertSame([$category111], $category11->children);
38 | }
39 |
40 | /** @test */
41 | public function canBeGeneratedFromAssocForm()
42 | {
43 | $relation = Relation::createRelation(Category::class, 'children', [
44 | Relation::OPT_CLASS => Category::class,
45 | Relation::OPT_OPPONENT => 'parent',
46 | ]);
47 |
48 | self::assertInstanceOf(ParentChildren::class, $relation);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/TestEntity.php:
--------------------------------------------------------------------------------
1 | [],
14 | 'byNameSpace' => [],
15 | 'byParent' => [],
16 | 'last' => null,
17 | ];
18 |
19 | static::$resolver = null;
20 | }
21 |
22 | public function setBulkInsert($class, BulkInsert $bulkInsert)
23 | {
24 | $this->bulkInserts[$class] = $bulkInsert;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/Testing/CreateMockedEntityTest.php:
--------------------------------------------------------------------------------
1 | em = $this->ormInitMock();
25 | }
26 |
27 | protected function tearDown(): void
28 | {
29 | m::close();
30 | }
31 |
32 | /** @test */
33 | public function throwsWithoutInitializing()
34 | {
35 | TestEntityManager::resetStaticsForTest();
36 |
37 | self::expectException(Exception::class);
38 | self::expectExceptionMessage('No entity manager initialized');
39 |
40 | $this->ormCreateMockedEntity(Article::class);
41 | }
42 |
43 | /** @test */
44 | public function throwsIfClassIsNotAnEntity()
45 | {
46 | self::expectException(Exception\NoEntity::class);
47 | self::expectExceptionMessage(' is not a subclass of Entity');
48 |
49 | $this->ormCreateMockedEntity(self::class);
50 | }
51 |
52 | /** @test */
53 | public function returnsTheEntity()
54 | {
55 | $article = $this->ormCreateMockedEntity(Article::class);
56 |
57 | self::assertInstanceOf(Article::class, $article);
58 | }
59 |
60 | /** @test */
61 | public function setsTheData()
62 | {
63 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42, 'title' => 'Hello World!']);
64 |
65 | self::assertSame(42, $article->id);
66 | self::assertSame('Hello World!', $article->title);
67 | }
68 |
69 | /** @test */
70 | public function allowsUsingAttributeKeys()
71 | {
72 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42, 'wordCount' => 3231]);
73 |
74 | self::assertSame(['id' => 42, 'word_count' => 3231], $article->getData());
75 | self::assertSame(3231, $article->wordCount);
76 | }
77 |
78 | /** @test */
79 | public function updateTheMock()
80 | {
81 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42, 'title' => 'Hello World!']);
82 | $article->shouldReceive('save')->once();
83 |
84 | $this->updateEntity(Article::class, 42, ['title' => 'Don`t Panic!']);
85 |
86 | self::assertSame('Don`t Panic!', $article->title);
87 | }
88 |
89 | protected function updateEntity($class, $id, $data)
90 | {
91 | $entity = $this->em->fetch($class, $id);
92 | $entity->fill($data);
93 | $entity->save();
94 | }
95 |
96 | /** @test */
97 | public function allowsValidationInSetAttribute()
98 | {
99 | // Category has enabled validation
100 | $entity = $this->ormCreateMockedEntity(Category::class, ['name' => 'Foo Bar']);
101 |
102 | $this->em->getConnection()->shouldNotReceive('query');
103 |
104 | // This would usually query the database to find the max length of name
105 | $entity->name = 'This could be limited to 20 chars but here it is not...';
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/Testing/ExpectDeleteTest.php:
--------------------------------------------------------------------------------
1 | em = $this->ormInitMock();
21 | }
22 |
23 | protected function tearDown(): void
24 | {
25 | m::close();
26 | }
27 |
28 | /** @test */
29 | public function allowsDeleteOfEntity()
30 | {
31 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42]);
32 |
33 | $this->ormExpectDelete($article);
34 |
35 | $this->deleteArticle(42);
36 | }
37 |
38 | /** @test */
39 | public function allowsDeleteOfClass()
40 | {
41 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42]);
42 |
43 | $this->ormExpectDelete(Article::class);
44 |
45 | $this->deleteArticle(42);
46 | }
47 |
48 | protected function deleteArticle($id)
49 | {
50 | $article = $this->em->fetch(Article::class, $id);
51 | $this->em->delete($article);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Testing/ExpectFetchTest.php:
--------------------------------------------------------------------------------
1 | em = $this->ormInitMock();
23 | }
24 |
25 | protected function tearDown(): void
26 | {
27 | \Mockery::close();
28 | }
29 |
30 | /** @test */
31 | public function returnsFetcher()
32 | {
33 | $fetcher = $this->ormExpectFetch(Article::class);
34 |
35 | self::assertInstanceOf(EntityFetcher::class, $fetcher);
36 |
37 | \Mockery::resetContainer();
38 | }
39 |
40 | /** @test */
41 | public function mocksFetch()
42 | {
43 | $fetcher = $this->ormExpectFetch(Article::class);
44 |
45 | self::assertSame($fetcher, $this->em->fetch(Article::class));
46 | }
47 |
48 | /** @test */
49 | public function returnsNull()
50 | {
51 | $this->ormExpectFetch(Article::class);
52 |
53 | $fetcher = $this->em->fetch(Article::class);
54 | $result = $fetcher->one();
55 |
56 | self::assertNull($result);
57 | }
58 |
59 | /** @test */
60 | public function returnsEntities()
61 | {
62 | $articles = [new Article(), new Article()];
63 | $this->ormExpectFetch(Article::class, $articles);
64 |
65 | $fetcher = $this->em->fetch(Article::class);
66 | $result = $fetcher->all();
67 |
68 | self::assertSame($articles, $result);
69 | }
70 |
71 | /** @test */
72 | public function returnsCount()
73 | {
74 | $articles = [new Article(), new Article()];
75 | $this->ormExpectFetch(Article::class, $articles);
76 |
77 | $fetcher = $this->em->fetch(Article::class);
78 | $result = $fetcher->count();
79 |
80 | self::assertSame(2, $result);
81 | }
82 |
83 | /** @test */
84 | public function expectFetchOnEntity()
85 | {
86 | $categories = [new Category(), new Category()];
87 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42]);
88 | $this->ormExpectFetch(Category::class, $categories);
89 |
90 | $fetcher = $article->fetch('categories');
91 | $result = $fetcher->all();
92 |
93 | self::assertSame($categories, $result);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Testing/ExpectInsertTest.php:
--------------------------------------------------------------------------------
1 | em = $this->ormInitMock();
22 | }
23 |
24 | protected function tearDown(): void
25 | {
26 | \Mockery::close();
27 | }
28 |
29 | /** @test */
30 | public function allowsInsertOfSpecifiedClass()
31 | {
32 | $this->ormExpectInsert(Article::class, ['id' => 42]);
33 | $article = new Article();
34 |
35 | $article->save();
36 | }
37 |
38 | /** @test */
39 | public function doesNotAllowInsertsOfOtherClasses()
40 | {
41 | $this->ormExpectInsert(Category::class);
42 | $article = new Article();
43 |
44 | self::expectException(\BadMethodCallException::class);
45 | self::expectExceptionMessage('but no expectations were specified');
46 |
47 | try {
48 | $article->save();
49 | } catch (\Exception $e) {
50 | \Mockery::resetContainer();
51 | throw $e;
52 | }
53 | }
54 |
55 | /** @test */
56 | public function setsDefaultData()
57 | {
58 | $defaults = ['id' => 42, 'created' => date('c')];
59 | $this->ormExpectInsert(Article::class, $defaults);
60 | $article = new Article();
61 |
62 | $article->save();
63 |
64 | self::assertSame($defaults, $article->getData());
65 | }
66 |
67 | /** @test */
68 | public function doesNotOverwriteCurrentData()
69 | {
70 | $defaults = ['id' => 42, 'created' => date('c')];
71 | $this->ormExpectInsert(Article::class, $defaults);
72 | $article = new Article();
73 |
74 | $article->id = 1337;
75 | $article->created = date('c', strtotime('-1 Hour'));
76 | $article->save();
77 |
78 | self::assertNotEquals($defaults, $article->getData());
79 | }
80 |
81 | /** @test */
82 | public function emulatesAutoIncrementWithRandomValue()
83 | {
84 | $this->ormExpectInsert(Article::class);
85 | $article = new Article();
86 |
87 | $article->save();
88 |
89 | self::assertGreaterThan(0, $article->id);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Testing/ExpectUpdateTest.php:
--------------------------------------------------------------------------------
1 | em = $this->ormInitMock();
21 | }
22 |
23 | protected function tearDown(): void
24 | {
25 | m::close();
26 | }
27 |
28 | /** @test */
29 | public function expectsSave()
30 | {
31 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42]);
32 | $this->ormExpectUpdate($article);
33 |
34 | self::expectException(m\Exception\InvalidCountException::class);
35 |
36 | m::close();
37 | }
38 |
39 | /** @test */
40 | public function doesNotEmulateUpdateWhenNotDirty()
41 | {
42 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42]);
43 | $this->ormExpectUpdate($article);
44 |
45 | $article->shouldNotReceive('preUpdate');
46 |
47 | $this->updateEntity(Article::class, 42);
48 | }
49 |
50 | /** @test */
51 | public function updatesTheDataFromDatabase()
52 | {
53 | $article = $this->ormCreateMockedEntity(Article::class, ['id' => 42, 'title' => 'Hello World!']);
54 | $this->ormExpectUpdate($article, [], ['title' => 'Don`t Panic!']);
55 |
56 | $article->shouldNotReceive('preUpdate');
57 |
58 | $this->updateEntity(Article::class, 42, ['title' => 'Don`t Panic!']);
59 | }
60 |
61 | /** @test */
62 | public function emulatesUpdate()
63 | {
64 | $article = $this->ormCreateMockedEntity(Article::class, [
65 | 'id' => 42,
66 | 'title' => 'Hello World!',
67 | 'changed' => date('c', strtotime('-1 Hour')),
68 | ]);
69 | $changingData = ['changed' => date('c')];
70 | $this->ormExpectUpdate($article, $changingData);
71 |
72 | $article->shouldReceive('preUpdate')->once()->ordered()->passthru();
73 | $article->shouldReceive('setOriginalData')->with(m::subset(array_merge(
74 | $article->getData(),
75 | ['title' => 'Don`t Panic!'],
76 | $changingData
77 | )))->once()->ordered()->passthru();
78 | $article->shouldReceive('reset')->once()->ordered()->passthru();
79 | $article->shouldReceive('postUpdate')->once()->ordered()->passthru();
80 |
81 | $this->updateEntity(Article::class, 42, ['title' => 'Don`t Panic!']);
82 |
83 | // self::assertSame($changingData['changed'], $article->changed);
84 | }
85 |
86 | /**
87 | * Update an entity with $data
88 | *
89 | * @param string $class
90 | * @param int $id
91 | * @param array $data
92 | */
93 | protected function updateEntity($class, $id, $data = [])
94 | {
95 | $entity = $this->em->fetch($class, $id);
96 | $entity->fill($data);
97 | $entity->save();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Testing/InitMockTest.php:
--------------------------------------------------------------------------------
1 | ormInitMock();
21 |
22 | self::assertInstanceOf(EntityManager::class, $em);
23 | }
24 |
25 | /** @test */
26 | public function entityManagerIsAMock()
27 | {
28 | $em = $this->ormInitMock();
29 |
30 | self::assertInstanceOf(MockInterface::class, $em);
31 | }
32 |
33 | /** @test */
34 | public function setsOptions()
35 | {
36 | $em = $this->ormInitMock(['tableNameTemplate' => '%short%s']);
37 |
38 | self::assertSame('articles', $em->getNamer()->getTableName(Article::class));
39 | }
40 |
41 | /** @test */
42 | public function mocksConnection()
43 | {
44 | $em = $this->ormInitMock();
45 | $connection = $em->getConnection();
46 |
47 | self::assertInstanceOf(MockInterface::class, $em);
48 | }
49 |
50 | /** @test */
51 | public function mocksSetAttribute()
52 | {
53 | $em = $this->ormInitMock();
54 | $connection = $em->getConnection();
55 |
56 | $result = $connection->setAttribute(\PDO::ATTR_AUTOCOMMIT, true);
57 |
58 | self::assertTrue($result);
59 | }
60 |
61 | /** @test */
62 | public function mocksDriverName()
63 | {
64 | $em = $this->ormInitMock([], 'mssql');
65 | $connection = $em->getConnection();
66 |
67 | $result = $connection->getAttribute(\PDO::ATTR_DRIVER_NAME);
68 |
69 | self::assertSame('mssql', $result);
70 | }
71 |
72 | /** @test */
73 | public function mocksQuote()
74 | {
75 | $em = $this->ormInitMock();
76 | $connection = $em->getConnection();
77 |
78 | $result = $connection->quote('Wayne\'s World!');
79 |
80 | self::assertSame('\'Wayne\\\'s World!\'', $result);
81 | }
82 |
83 | /** @test */
84 | public function createsAnEntityFetcherMock()
85 | {
86 | $em = $this->ormInitMock();
87 |
88 | $fetcher = $em->fetch(Article::class);
89 |
90 | self::assertInstanceOf(EntityFetcherMock::class, $fetcher);
91 | }
92 |
93 | /** @test */
94 | public function returnsMappedEntities()
95 | {
96 | $em = $this->ormInitMock();
97 | $em->map($original = new Article(['id' => 23]));
98 |
99 | $article = $em->fetch(Article::class, 23);
100 |
101 | self::assertSame($original, $article);
102 | }
103 |
104 | /** @test */
105 | public function returnsNullWithPrimaryKey()
106 | {
107 | $em = $this->ormInitMock();
108 |
109 | $article = $em->fetch(Article::class, 23);
110 |
111 | self::assertNull($article);
112 | }
113 |
114 | /** @test */
115 | public function throwsWhenTheClassIsNotAnEntity()
116 | {
117 | $em = $this->ormInitMock();
118 |
119 | self::expectException(NoEntity::class);
120 |
121 | $em->fetch(NoEntity::class);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------