├── .codeclimate.yml ├── .coveralls.yml ├── .csslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build └── logs │ └── .gitignore ├── composer.json ├── doc ├── filemaker │ ├── .gitignore │ ├── FMServer_Sample_web.fmp12 │ ├── README.md │ ├── XmlSchemaDemo.fmp12 │ ├── composer.json │ ├── fms15_cwp_guide-XML-grammar.pdf │ ├── fms15_cwp_guide.pdf │ ├── fms15_getting_started.pdf │ └── simplefm_example.php └── src │ ├── authentication.md │ ├── debugging.md │ ├── getting-started.md │ ├── index.md │ ├── layout-information.md │ └── repositories │ ├── automatic-creation.md │ ├── introduction.md │ ├── manual-creation.md │ ├── metadata.md │ └── working-with-repositories.md ├── mkdocs.yml ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── Authentication │ ├── Authenticator.php │ ├── BlockCipherIdentityHandler.php │ ├── Exception │ │ ├── ExceptionInterface.php │ │ └── InvalidResultException.php │ ├── Identity.php │ ├── IdentityHandlerInterface.php │ └── Result.php ├── Client │ ├── Exception │ │ ├── ExceptionInterface.php │ │ └── FileMakerException.php │ ├── Layout │ │ ├── Field.php │ │ ├── Layout.php │ │ ├── LayoutClient.php │ │ ├── LayoutClientInterface.php │ │ ├── Value.php │ │ └── ValueList.php │ └── ResultSet │ │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── ParseException.php │ │ └── UnknownFieldException.php │ │ ├── ResultSetClient.php │ │ ├── ResultSetClientInterface.php │ │ └── Transformer │ │ ├── ContainerTransformer.php │ │ ├── DateTimeTransformer.php │ │ ├── DateTransformer.php │ │ ├── Exception │ │ ├── DateTimeException.php │ │ └── ExceptionInterface.php │ │ ├── NumberTransformer.php │ │ ├── StreamProxy.php │ │ ├── TextTransformer.php │ │ └── TimeTransformer.php ├── Collection │ ├── CollectionInterface.php │ └── ItemCollection.php ├── Connection │ ├── Command.php │ ├── Connection.php │ ├── ConnectionInterface.php │ └── Exception │ │ ├── DomainException.php │ │ ├── ExceptionInterface.php │ │ └── InvalidResponseException.php └── Repository │ ├── Builder │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── ExtractionException.php │ │ └── HydrationException.php │ ├── Metadata │ │ ├── Embeddable.php │ │ ├── Entity.php │ │ ├── Exception │ │ │ ├── ExceptionInterface.php │ │ │ ├── InvalidFileException.php │ │ │ ├── InvalidTypeException.php │ │ │ └── MissingInterfaceException.php │ │ ├── Field.php │ │ ├── ManyToOne.php │ │ ├── MetadataBuilder.php │ │ ├── MetadataBuilderInterface.php │ │ ├── OneToMany.php │ │ ├── OneToOne.php │ │ └── RecordId.php │ ├── MetadataExtraction.php │ ├── MetadataHydration.php │ ├── Proxy │ │ ├── Exception │ │ │ ├── ExceptionInterface.php │ │ │ └── InvalidInterfaceException.php │ │ ├── ProxyBuilder.php │ │ ├── ProxyBuilderInterface.php │ │ └── ProxyInterface.php │ ├── RepositoryBuilder.php │ ├── RepositoryBuilderInterface.php │ └── Type │ │ ├── BooleanType.php │ │ ├── DateTimeType.php │ │ ├── DateType.php │ │ ├── DecimalType.php │ │ ├── Exception │ │ ├── DomainException.php │ │ └── ExceptionInterface.php │ │ ├── FloatType.php │ │ ├── IntegerType.php │ │ ├── NullableStringType.php │ │ ├── StreamType.php │ │ ├── StringType.php │ │ ├── TimeType.php │ │ └── TypeInterface.php │ ├── Exception │ ├── DomainException.php │ ├── ExceptionInterface.php │ └── InvalidResultException.php │ ├── ExtractionInterface.php │ ├── HydrationInterface.php │ ├── LazyLoadedCollection.php │ ├── Query │ ├── Exception │ │ ├── EmptyQueryException.php │ │ ├── ExceptionInterface.php │ │ └── InvalidArgumentException.php │ ├── FindQuery.php │ └── Query.php │ ├── Repository.php │ └── RepositoryInterface.php ├── test ├── Authentication │ ├── AuthenticationTest.php │ ├── BlockCipherIdentityHandlerTest.php │ ├── IdentityTest.php │ └── ResultTest.php ├── Client │ ├── Layout │ │ ├── LayoutClientTest.php │ │ └── TestAssets │ │ │ ├── error_fmpxmllayout.xml │ │ │ └── sample_fmpxmllayout.xml │ └── ResultSet │ │ ├── ResultSetClientTest.php │ │ ├── TestAssets │ │ ├── ParentChildAssociations │ │ │ ├── Base-recid1-id2.xml │ │ │ ├── Base-recid2-id3.xml │ │ │ ├── Base_sparse-collection.xml │ │ │ ├── Child-recid1-id1.xml │ │ │ ├── Child-recid2-id2.xml │ │ │ ├── Child-recid3-id3.xml │ │ │ └── Parent-recid1-id2.xml │ │ ├── deleted-field.xml │ │ ├── duplicateportals.xml │ │ ├── invalid.xml │ │ ├── invalidFieldTypeDate.xml │ │ ├── invalidFieldTypeFake.xml │ │ ├── invalidFieldTypeRepeatingNumber.xml │ │ ├── invalidFieldTypeTime.xml │ │ ├── invalidFieldTypeTimestamp.xml │ │ ├── invalidFieldTypes.xml │ │ ├── projectsampledata.xml │ │ ├── reservedfieldname.xml │ │ ├── sample_fmresultset.xml │ │ ├── sample_fmresultset_empty.xml │ │ └── sample_fmresultset_fmerror4.xml │ │ └── Type │ │ ├── NumberTransformerTest.php │ │ └── StreamProxyTest.php ├── Collection │ └── ItemCollectionTest.php ├── Connection │ ├── CommandTest.php │ └── ConnectionTest.php └── Repository │ ├── Builder │ ├── Metadata │ │ ├── EmbeddableTest.php │ │ ├── EntityTest.php │ │ ├── FieldTest.php │ │ ├── ManyToOneTest.php │ │ ├── MetadataBuilderTest.php │ │ ├── OneToManyTest.php │ │ ├── OneToOneTest.php │ │ ├── RecordIdTest.php │ │ └── TestAssets │ │ │ ├── BuiltInTypes.xml │ │ │ ├── CustomType.xml │ │ │ ├── EagerHydration.xml │ │ │ ├── Embeddable.xml │ │ │ ├── EmbeddedEntity.xml │ │ │ ├── Empty.xml │ │ │ ├── InterfaceName.xml │ │ │ ├── Invalid.Xml.xml │ │ │ ├── ManyToOne.xml │ │ │ ├── ManyToOneWithoutInterface.xml │ │ │ ├── Non.Xml.xml │ │ │ ├── OneToMany.xml │ │ │ ├── OneToOneInverse.xml │ │ │ ├── OneToOneOwning.xml │ │ │ ├── ReadOnly.xml │ │ │ ├── RecordId.xml │ │ │ ├── RelationTarget.xml │ │ │ ├── RelationTargetWithoutInterface.xml │ │ │ └── Repeatable.xml │ ├── MetadataExtractionTest.php │ ├── MetadataHydrationTest.php │ ├── Proxy │ │ ├── ProxyBuilderTest.php │ │ └── TestAssets │ │ │ ├── ComplexSetterInterface.php │ │ │ ├── SimpleGetterInterface.php │ │ │ ├── SimpleSetterInterface.php │ │ │ └── VariadicSetterInterface.php │ ├── RepositoryBuilderTest.php │ ├── TestAssets │ │ ├── EmptyEntityInterface.php │ │ └── EmptyProxyEntityInterface.php │ └── Type │ │ ├── BooleanTypeTest.php │ │ ├── DateTimeTypeTest.php │ │ ├── DateTypeTest.php │ │ ├── DecimalTypeTest.php │ │ ├── FloatTypeTest.php │ │ ├── IntegerTypeTest.php │ │ ├── NullableStringTypeTest.php │ │ ├── StreamTypeTest.php │ │ ├── StringTypeTest.php │ │ └── TimeTypeTest.php │ ├── LazyLoadedCollectionTest.php │ ├── Query │ ├── FindQueryTest.php │ └── QueryTest.php │ └── RepositoryTest.php └── xsd ├── entity-metadata-5-0.xsd └── entity-metadata-5-1.xsd /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - ruby 10 | - javascript 11 | - python 12 | - php 13 | fixme: 14 | enabled: true 15 | phpmd: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.css" 20 | - "**.inc" 21 | - "**.js" 22 | - "**.jsx" 23 | - "**.module" 24 | - "**.php" 25 | - "**.py" 26 | - "**.rb" 27 | exclude_paths: 28 | - documentation/* 29 | - tests/* 30 | - vendor/* 31 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: clover.xml 2 | json_path: coveralls-upload.json 3 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /composer.phar 4 | /clover.xml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | - $HOME/.local 9 | - vendor 10 | 11 | matrix: 12 | fast_finish: true 13 | include: 14 | - php: 7 15 | env: 16 | - EXECUTE_CS_CHECK=true 17 | - EXECUTE_TEST_COVERALLS=true 18 | - EXECUTE_TEST_CODECLIMATE=true 19 | - PATH="$HOME/.local/bin:$PATH" 20 | - php: nightly 21 | - php: hhvm 22 | allow_failures: 23 | - php: nightly 24 | - php: hhvm 25 | 26 | before_install: 27 | - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]] && [[ $EXECUTE_TEST_CODECLIMATE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi 28 | - composer self-update 29 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer require --dev --no-update satooshi/php-coveralls:^1.0.0 ; fi 30 | - if [[ $EXECUTE_TEST_CODECLIMATE == 'true' ]]; then composer require --dev --no-update codeclimate/php-test-reporter:^0.3.2 ; fi 31 | 32 | install: 33 | - travis_retry composer install --no-interaction 34 | - composer info -i 35 | 36 | script: 37 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]] || [[ $EXECUTE_TEST_CODECLIMATE == 'true' ]]; then composer test-coverage ; fi 38 | - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then composer test ; fi 39 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi 40 | 41 | after_script: 42 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer coveralls ; fi 43 | - if [[ $EXECUTE_TEST_CODECLIMATE == 'true' ]]; then CODECLIMATE_REPO_TOKEN=a1fa5b53529cf77226b6231a9c0a3d8ce5c0cb9ecc570ac014c1cc60fa07d2df ./vendor/bin/test-reporter --coverage-report=clover.xml --stdout > codeclimate.json ; fi 44 | - "if [[ $EXECUTE_TEST_CODECLIMATE == 'true' ]]; then curl -X POST -d @codeclimate.json -H 'Content-Type: application/json' -H 'User-Agent: Code Climate (PHP Test Reporter v0.1.1)' https://codeclimate.com/test_reports ; fi" 45 | 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone! Here are some basic tips and tricks for constructive contribution. 4 | 5 | ## Fork, Clone and Install 6 | 7 | Fork, then clone the repo: 8 | 9 | git clone git@github.com:your-username/SimpleFM.git 10 | 11 | If you don't have composer installed in your path already, you can install it inside the cloned project with this 12 | command: 13 | 14 | php -r "readfile('https://getcomposer.org/installer');" | php 15 | 16 | We suggest you always use [Composer](https://getcomposer.org/) `update` in the project (as opposed to `install`). 17 | Since this is a library, we `.gitignore` both `composer.phar` and `composer.lock`: 18 | 19 | php ./composer.phar update 20 | 21 | ## Unit Tests and Coding Style Tests 22 | 23 | The project is setup to run all the PHPUnit and PHPCS tests via 24 | [Apache ant](http://ant.apache.org/manual/install.html). (See `build.xml` in the project root.) 25 | 26 | Before you start make sure the tests run using ant: 27 | 28 | ant 29 | 30 | Alternatively, make sure the tests run like this if you don't want to use ant: 31 | 32 | php ./vendor/bin/phpunit -c tests/phpunit.xml.dist 33 | php ./vendor/bin/phpcs -np --standard=PSR2 library/ tests/ 34 | 35 | ## Branch, Change and Test 36 | 37 | Before you make your changes, please create a new branch. Example: 38 | 39 | git checkout -b feature/my-thing 40 | 41 | Make your change. Add tests for your change. Make sure the tests pass (or run both manually as above): 42 | 43 | ant 44 | 45 | ## Create Pull Request 46 | 47 | Push your new branch to your fork and [submit a pull request][pr]. 48 | 49 | [pr]: https://github.com/soliantconsulting/SimpleFM/compare/ 50 | 51 | At this point you're waiting on us. We try to at least comment on pull requests within three business days (and, 52 | typically, one business day). If you don't get any response within three days, feel free to bump it with a comment. We 53 | may suggest some changes or improvements or alternatives. 54 | 55 | ## Tips 56 | 57 | Some things that will increase the chance that your pull request is accepted: 58 | 59 | * Don't break existing tests. 60 | * Write test coverage for your change(s). 61 | * Follow the PSR-2 [coding standards][style]. 62 | * Write [good commit messages][commit]. 63 | * Explain and/or justify the reason for the change in your PR description. 64 | 65 | [style]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 66 | [commit]: https://git-scm.com/book/ch5-2.html#Commit-Guidelines 67 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2007-2015 Soliant Consulting, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 9 | Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleFM 2 | 3 | [![Build Status](https://travis-ci.org/soliantconsulting/SimpleFM.svg?branch=master)](https://travis-ci.org/soliantconsulting/SimpleFM) 4 | [![Code Climate](https://codeclimate.com/github/soliantconsulting/SimpleFM/badges/gpa.svg)](https://codeclimate.com/github/soliantconsulting/SimpleFM) 5 | [![Test Coverage](https://codeclimate.com/github/soliantconsulting/SimpleFM/badges/coverage.svg)](https://codeclimate.com/github/soliantconsulting/SimpleFM/coverage) 6 | [![Latest Stable Version](https://poser.pugx.org/soliantconsulting/simplefm/v/stable)](https://packagist.org/packages/soliantconsulting/simplefm) 7 | [![Latest Unstable Version](https://poser.pugx.org/soliantconsulting/simplefm/v/unstable)](https://packagist.org/packages/soliantconsulting/simplefm) 8 | [![Total Downloads](https://poser.pugx.org/soliantconsulting/simplefm/downloads)](https://packagist.org/packages/soliantconsulting/simplefm) 9 | [![License](https://poser.pugx.org/soliantconsulting/simplefm/license)](https://packagist.org/packages/soliantconsulting/simplefm) 10 | 11 | SimpleFM is a fast, convenient and free tool designed by [Soliant Consulting, Inc.][1] to facilitate connections between PHP web applications and FileMaker Server. 12 | 13 | SimpleFM is a lightweight PHP package that uses the [FileMaker Server][2] XML API. The FMS XML API is commonly referred to as Custom Web Publishing (CWP for short). 14 | 15 | SimpleFM is [Composer][3] friendly, making it a snap to use with all [PHP-FIG][4] frameworks, including [Zend Framework][5], Symfony, Laravel, Slim, and many more. 16 | 17 | See also, the [SimpleFM-skeleton][6] demo application which illustrates use of SimpleFM in a middleware Zend Framework application. 18 | 19 | ## Features 20 | 21 | ### Easy to Integrate 22 | 23 | - PSR-4 autoloading ([Composer][3] ready). 24 | - Can be used on it's own or with any service or middleware, such as [Apigility][7] or Stratigility. 25 | 26 | ### CWP Debugger 27 | 28 | - Easily see the underlying API command formatted as a URL for easy troubleshooting 29 | - FileMaker error codes are translated to understandable error messages 30 | 31 | ## Simplicity and Performance 32 | 33 | SimpleFM was written with simplicity as the guiding principle. We have informally benchmarked it, and obtained faster results for the same queries compared to the two most common CWP PHP alternatives. 34 | 35 | ## System Requirements 36 | 37 | SimpleFM, the examples and this documentation are tailored for PHP 7.0 and FileMaker Sever 12 38 | 39 | - PHP 7.0+ 40 | - FileMaker Server 12+ 41 | 42 | With minimum effort, it should theoretically work with any version of FileMaker server that uses fmresultset.xml grammar, however, backward compatibility is not verified or maintained. 43 | 44 | ## License 45 | 46 | SimpleFM is free for commercial and non-commercial use, licensed under the business-friendly standard MIT license. 47 | 48 | ## Installation 49 | 50 | Install via composer: 51 | 52 | ```bash 53 | composer require soliantconsulting/simplefm 54 | ``` 55 | 56 | ## Documentation 57 | 58 | Documentation builds are available at: 59 | 60 | - https://simplefm.readthedocs.org 61 | 62 | You can also build the documentation locally via [MkDocs](http://www.mkdocs.org): 63 | 64 | ```bash 65 | $ mkdocs serve 66 | ``` 67 | 68 | [1]: http://www.soliantconsulting.com 69 | [2]: http://www.filemaker.com/products/filemaker-server/ 70 | [3]: https://getcomposer.org/doc/00-intro.md 71 | [4]: http://www.php-fig.org/ 72 | [5]: http://framework.zend.com/ 73 | [6]: https://github.com/soliantconsulting/SimpleFM-skeleton 74 | [7]: https://apigility.org/ 75 | -------------------------------------------------------------------------------- /build/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soliantconsulting/simplefm", 3 | "type": "library", 4 | "description": "FileMaker Server XML API Adapter", 5 | "keywords": [ 6 | "FileMaker", 7 | "adapter" 8 | ], 9 | "homepage": "http://github.com/soliantconsulting/SimpleFM", 10 | "license": "MIT", 11 | "authors": [ 12 | {"name": "Jeremiah Small", "email": "jsmall@soliantconsulting.com"}, 13 | {"name": "Ben Scholzen", "email": "bscholzen@soliantconsulting.com"} 14 | ], 15 | "require": { 16 | "php": "^7.0", 17 | "php-http/httplug": "^1.1", 18 | "zendframework/zend-diactoros": "^1.3", 19 | "litipk/php-bignumbers": "^0.8.4", 20 | "beberlei/assert": "^2.6", 21 | "psr/log": "^1.0", 22 | "zendframework/zend-code": "^3.0", 23 | "psr/cache": "^1.0", 24 | "cache/void-adapter": "^0.3.1", 25 | "cache/adapter-common": "^0.3.3" 26 | }, 27 | "require-dev": { 28 | "zendframework/zend-crypt": "^3.1", 29 | "phpunit/phpunit": "^5.5", 30 | "squizlabs/php_codesniffer": "^2.6" 31 | }, 32 | "suggest": { 33 | "zendframework/zend-crypt": "For using the block chain identity handler" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Soliant\\SimpleFM\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "SoliantTest\\SimpleFM\\": "test/" 43 | } 44 | }, 45 | "scripts": { 46 | "check": [ 47 | "@cs", 48 | "@test" 49 | ], 50 | "coveralls": "coveralls", 51 | "cs": "phpcs", 52 | "cs-fix": "phpcbf", 53 | "test": "phpunit", 54 | "test-coverage": "phpunit --coverage-clover clover.xml", 55 | "html": "phpunit --coverage-html build/logs/coverage.html", 56 | "deploy-xsd": "git subtree push --prefix xsd origin gh-pages" 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /doc/filemaker/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /doc/filemaker/FMServer_Sample_web.fmp12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soliantconsulting/SimpleFM/ab453bc87f8ca61ed3fa75ee61a8ab5851fc9fe7/doc/filemaker/FMServer_Sample_web.fmp12 -------------------------------------------------------------------------------- /doc/filemaker/README.md: -------------------------------------------------------------------------------- 1 | Parent Child Association Test Data 2 | ================================== 3 | 4 | Data Model 5 | ---------- 6 | 7 | ``` 8 | 9 | -------- ------ ------- 10 | | Parent | -< | Base | -< | Child | 11 | -------- ------ ------- 12 | 13 | ``` 14 | 15 | Static XML Result Test Assets 16 | ----------------------------- 17 | 18 | ``` 19 | tests/SoliantTest/SimpleFM/TestAssets/ParentChildAssociations 20 | ``` 21 | 22 | Example FileMaker File 23 | ---------------------- 24 | 25 | ``` 26 | documentation/XmlSchemaDemo.fmp12 27 | ``` 28 | Developer credentials are admin:admin 29 | -------------------------------------------------------------------------------- /doc/filemaker/XmlSchemaDemo.fmp12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soliantconsulting/SimpleFM/ab453bc87f8ca61ed3fa75ee61a8ab5851fc9fe7/doc/filemaker/XmlSchemaDemo.fmp12 -------------------------------------------------------------------------------- /doc/filemaker/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soliantconsulting/simplefm-example", 3 | "type": "project", 4 | "description": "FileMaker Server XML API Adapter", 5 | "keywords": [ 6 | "FileMaker", 7 | "adapter" 8 | ], 9 | "homepage": "http://github.com/soliantconsulting/SimpleFM", 10 | "license": "MIT", 11 | "authors": [ 12 | {"name": "Jeremiah Small", "email": "jsmall@soliantconsulting.com"} 13 | ], 14 | "require": { 15 | "soliantconsulting/simplefm": "dev-develop", 16 | "php-http/curl-client": "^1.6", 17 | "monolog/monolog": "^1.9" 18 | }, 19 | "scripts": { 20 | "serve": "echo run it manually: php -S 0.0.0.0:8080 -t ./ ./simplefm_example.php" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/filemaker/fms15_cwp_guide-XML-grammar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soliantconsulting/SimpleFM/ab453bc87f8ca61ed3fa75ee61a8ab5851fc9fe7/doc/filemaker/fms15_cwp_guide-XML-grammar.pdf -------------------------------------------------------------------------------- /doc/filemaker/fms15_cwp_guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soliantconsulting/SimpleFM/ab453bc87f8ca61ed3fa75ee61a8ab5851fc9fe7/doc/filemaker/fms15_cwp_guide.pdf -------------------------------------------------------------------------------- /doc/filemaker/fms15_getting_started.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soliantconsulting/SimpleFM/ab453bc87f8ca61ed3fa75ee61a8ab5851fc9fe7/doc/filemaker/fms15_getting_started.pdf -------------------------------------------------------------------------------- /doc/src/authentication.md: -------------------------------------------------------------------------------- 1 | # Authenticating a user 2 | 3 | SimpleFM supplies you with the tools to authenticate a user against the server an run requests under their permissions. 4 | Since the user's name and password are required for making requests on their behalf, it needs to be stored temporarily. 5 | When you authenticate the user against the server, it will return an identity which contains both the username and the 6 | password in an encrypted form. An identity handler is responsible for encrypting and decrypting the password, and while 7 | you can implement your own one, SimpleFM supplies a block chipher identity handler based on `Zend\Crypt`. 8 | 9 | Using the built-in identity handler, you can quickly create an authenticator like this: 10 | 11 | ``` 12 | 'aes'])->setKey('some-strong-encryption-key') 19 | ); 20 | 21 | $authenticator = new Authenticator( 22 | $resultSetClient, 23 | $identityHandler, 24 | 'identity-layout', 25 | 'username-field' 26 | ); 27 | ``` 28 | 29 | With the authenticator created you are able to authenticator the user against it. It will return a `Result` object, 30 | which will contain the identity on success: 31 | 32 | ```php 33 | authenticate($username, $password); 35 | 36 | if (!$result->isSuccess()) { 37 | // Prompt the user to enter their credentials again 38 | } 39 | 40 | $identity = $result->getIdentity(); 41 | ``` 42 | 43 | You should store the returned identity, either in a session or in a cookie. Now before you can make requests with that 44 | identity, you have to pass the identity handler to the connection first. To do so, you pass it as fourth parameter to 45 | the constructor: 46 | 47 | ```php 48 | execute( 65 | (new Command('sample-layout', ['-findall' => null]))->withIdentity($identity) 66 | ); 67 | ``` 68 | 69 | This would return all records which the user has permission to see. Since you will normally not run commands directly 70 | but via a repository, you'd want to run requests through those instead: 71 | 72 | ```php 73 | withIdentity($identity)->findAll(); 75 | ``` 76 | 77 | As any object within SimpleFM is immutable, the `withIdentity()` method actually returns a clone of the repository with 78 | the identity set to it. You can re-use that clone if you need to run multiple requests. 79 | 80 | -------------------------------------------------------------------------------- /doc/src/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging requests 2 | 3 | Sometimes it can be really hard to debug a request without looking at the actual XML result directly. To make this task 4 | a little easier, you can provide a PSR-3 compliant logger to the connection. This will result in each request URL being 5 | logged, with the request parameters appended in the query. 6 | 7 | Many frameworks and libraries provide loggers implementing PSR-3, so you have a wide variety to choose from. In the 8 | following example we will use the PSR-3 adapter of `Zend\Log`: 9 | 10 | ```php 11 | [ 19 | new Stream('/path/to/logfile') 20 | ], 21 | ]) 22 | ); 23 | 24 | $connection = new Connection( 25 | $httpClient, 26 | $uri, 27 | $databsae, 28 | null, 29 | $logger 30 | ); 31 | ``` 32 | 33 | !!!note 34 | Since a logger will cause I/O for every single request made against the FileMaker Server, you should **not** enable 35 | this in production. 36 | -------------------------------------------------------------------------------- /doc/src/getting-started.md: -------------------------------------------------------------------------------- 1 | # Creating a connection 2 | 3 | The first thing you want to do is to create a connection which can be used with either the layout client or the 4 | result set client. While you can always implement your own connection class by implementing the `ConnectionInterface`, 5 | most of the time the built-in one should be sufficient. 6 | 7 | To create an instance of a connection, you need an instance of an HTTP client implementing the 8 | [`HttpClient` interface](https://github.com/php-http/httplug/blob/master/src/HttpClient.php). There are many different 9 | implementations already created, so you can choose [one from php-http](https://github.com/php-http). For the sake of 10 | simplicity, we are going with the CURL client here. 11 | 12 | The next thing you need is a PSR-7 URI object. Again, there are many different implementations for that out in the 13 | world, and we will use the one shipped with `Zend\Diactoros`. The URI object should contain the scheme, host and 14 | optionally a port and an authority part. 15 | 16 | ```php 17 | execute(new Command('sample-layout', ['-findall' => null])); 54 | 55 | foreach ($records as $record) { 56 | // Do something with the record. 57 | } 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /doc/src/index.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /doc/src/layout-information.md: -------------------------------------------------------------------------------- 1 | # Retrieving information about a layout 2 | 3 | Sometimes you need to programatically retrieve information about a specific layout, like the available fields, field set 4 | options and the like. For this purpose SimpleFM provides a layout client, which can be used similary to the result set 5 | client: 6 | 7 | ```php 8 | execute(new Command( 14 | 'sample-layout', 15 | ['-view' => null] 16 | )); 17 | ``` 18 | 19 | The result will be a `Layout` object, which contains all information about the layout and its fields: 20 | 21 | ```php 22 | getDatabase()); 24 | sprintf("Layout: %s\n", $sampleLayout->getName()); 25 | 26 | print "Fields:\n"; 27 | 28 | foreach ($sampleLayout->getFields() as $field) { 29 | sprintf(" Name: %s\n", $field->getName()); 30 | sprintf(" Type: %s\n", $field->getType()); 31 | 32 | if (!$field->hasValueList()) { 33 | continue; 34 | } 35 | 36 | $valueList = $field->getValueList(); 37 | sprintf(" Value list (%s):\n", $valueList->getName()); 38 | 39 | foreach ($valueList->getValues() as $value) { 40 | sprintf(" %s: %s\n", $value->getValue(), $value->getDisplay()); 41 | } 42 | } 43 | ``` 44 | 45 | You can also retrieve individual fields instead of looping over all fields: 46 | 47 | ```php 48 | hasField('foo')) { 50 | $field = $sampleLayout->getField('foo'); 51 | } 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /doc/src/repositories/automatic-creation.md: -------------------------------------------------------------------------------- 1 | # Getting rid of manual code 2 | 3 | While it is feasible to create all your repositories manully, it can become a daunting task to do this for a lot of 4 | entities. Thus it is recommended to only define the metadata about your entity, and let SimpleFM create the 5 | repositories and their hydration and extraction strategies for you automatically. 6 | 7 | # Defining your metadata file 8 | 9 | The first thing you want to do is to create your metadata file. This is a simple XML file which describes all fields 10 | and relations of the entity in the given layout. You should choose one folder in which you place all those XML files, 11 | and each XML file must have a filename which equals the class name of the entity, with the namespace delimiters replaced 12 | by periods. So when your entity is named `My\Entity\SampleEntity`, the acccording filename would be 13 | `My.Entity.SampleEntity.xml`: 14 | 15 | ```xml 16 | 17 | 25 | 26 | 27 | ``` 28 | 29 | You can read more about metadata definitions in the the [Metadata chapter](repositories/metadata.md) 30 | 31 | # Creating a repository builder 32 | 33 | Now that we have our metadata defined, we only need to instantiate a repository builder for it: 34 | 35 | ```php 36 | buildRepository(SampleEntity::class); 55 | ``` 56 | -------------------------------------------------------------------------------- /doc/src/repositories/introduction.md: -------------------------------------------------------------------------------- 1 | # Easier access to data 2 | 3 | While very simple applications can work fine just using the result set client, it is a best practice to organize data 4 | into entities with associations. To facilitate this, SimpleFM provides repositories which take care of the internals 5 | and let the application developer concentrate on the business domain. 6 | 7 | A repository instance is responsible for a single type of entity. It is where you define the infrastructure to insert, 8 | update, and delete an entity, as well as giving you multiple ways to query. 9 | 10 | The FileMaker XML API exposes data via layouts. We'll come back to this in more detail later, but it's important to know 11 | that entities, repositories, and layouts must be created and maintained in concert with each other. 12 | -------------------------------------------------------------------------------- /doc/src/repositories/manual-creation.md: -------------------------------------------------------------------------------- 1 | # Creating a hydrator and extractor 2 | 3 | For creating a repository completely manually, you have to define a hydrator and an extractor which take care of 4 | converting an entity to record data and visa-versa. Let's say that you have a layout with a simple `name` field, these 5 | could look something like this: 6 | 7 | ```php 8 | setName($data['name']); 22 | } 23 | } 24 | 25 | final class SampleExtraction implements ExtractionInterface 26 | { 27 | public function extract($entity) : array 28 | { 29 | return [ 30 | 'name' => $entity->getName(), 31 | ]; 32 | } 33 | } 34 | ``` 35 | 36 | # Initializing a repository 37 | 38 | With these two classes and your result set client in place, you can craft your very first repository: 39 | 40 | ```php 41 | insert(new SampleEntity('test')); 9 | ``` 10 | 11 | Similary, when you want to update the record, you'd call the `update()` method: 12 | 13 | ```php 14 | update($changedSampleEntity); 16 | ``` 17 | 18 | In the same way, you can also delete a record from the layout: 19 | 20 | ```php 21 | delete($oldSampleEntity); 23 | ``` 24 | 25 | Please note that both `update()` and `delete()` only work with entities which you retrieved via the repository (also 26 | known as managed entities). If you try to pass an entity to those methods which is not known by the repository, you will 27 | get an exception. By default, both `update()` and `delete()` will instruct the FileMaker Server to only execute that 28 | command when the `mod-id` is the same as the one from the local entity to avoid changing records which were modified in 29 | another place. You can disable that behaviour by passing `true` to the second `$force` parameter of those methods. 30 | 31 | # Retrieving entities through a simple search 32 | 33 | Repositories offer multiple ways, to retrieve entites. The simplest one is by calling the `find()` method with a record 34 | ID of the record you want to retrieve. The result will either be an instance of that entity or null: 35 | 36 | ```php 37 | find(1); 39 | ``` 40 | 41 | Similary, there is a `findBy()` and a `findOneBy()` method for either retrieving multiple records or a single one with 42 | a simple field search. Both methods take an array field search values, where the array key is the field name and the 43 | array value is the actual search. By default, the search value is automatically quoted. If you need to perform more 44 | advanced searches, you can pass `false` to the `$autoQuoteSearch` parameter and do required quoting manually by passing 45 | the value to quote to the `quoteString()` method. For details about the different search options, please refer to the 46 | FileMaker documentation. 47 | 48 | If you only want to retrieve all records from the layout without filterinig, there's also a `findAll()` method. All 49 | methods which return multiple records also accept a `$sort` parameter for sorting the results, as well as a `$limit` and 50 | `$offset` parameter. 51 | 52 | # Performing complex searches 53 | 54 | While the `findBy()` and `findOneBy()` methods will always perform an `AND` query. something you may want to make `OR` 55 | queries or mixed queries. To allow this, the repository exposes two methods, namely `findByQuery()` and 56 | `findOneByQuery()`. Both work similar to the prior two methods, but instead of a search array they take a `FindQuery` 57 | object: 58 | 59 | ```php 60 | addOrQueries( 66 | new Query('ID', '1'), 67 | new Query('ID', '2'), 68 | ); 69 | $query->addAndQueries( 70 | new Query('Name', 'foo'), 71 | new Query('Status', 'closed', true) 72 | ); 73 | 74 | $sampleEntities = $respository->findByQuery($query); 75 | ``` 76 | 77 | This will query will look for any record which `ID` is either 1 or 2 and which `Name` equals "foo", but the `Status` 78 | field must not be "closed". 79 | 80 | !!!note "Query quoting" 81 | Values in a find query are never quoted automatically, so you'll need to quote those values manually via the 82 | `quoteString()` method of the repository. 83 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: SimpleFM 2 | repo_url: https://github.com/soliantconsulting/SimpleFM/ 3 | copyright: Copyright (c) 2007-2016 Soliant Consulting, Inc. 4 | docs_dir: doc/src 5 | site_dir: doc/html 6 | theme: readthedocs 7 | pages: 8 | - index.md 9 | - 'Getting Started' : getting-started.md 10 | - 'Repositories' : 11 | [ 12 | 'Introduction' : repositories/introduction.md, 13 | 'Manual creation' : repositories/manual-creation.md, 14 | 'Automatic creation' : repositories/automatic-creation.md, 15 | 'Metadata' : repositories/metadata.md, 16 | 'Working with repositories' : repositories/working-with-repositories.md, 17 | ] 18 | - 'Authentication' : authentication.md 19 | - 'Debugging' : debugging.md 20 | - 'Layout Information' : layout-information.md 21 | 22 | markdown_extensions: 23 | - admonition: 24 | - def_list: 25 | - footnotes: 26 | 27 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SimpleFM coding standard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src 20 | test 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./test 9 | 10 | 11 | 12 | 13 | 14 | src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Authentication/Authenticator.php: -------------------------------------------------------------------------------- 1 | resultSetClient = $resultSetClient; 40 | $this->identityHandler = $identityHandler; 41 | $this->identityLayout = $identityLayout; 42 | $this->usernameField = $usernameField; 43 | } 44 | 45 | public function authenticate(string $username, string $password) : Result 46 | { 47 | $identity = $this->identityHandler->createIdentity($username, $password); 48 | 49 | try { 50 | $resultSet = $this->resultSetClient->execute( 51 | (new Command($this->identityLayout, [ 52 | $this->usernameField => '==' . $this->resultSetClient->quoteString($username), 53 | '-find' => null, 54 | ]))->withIdentity($identity) 55 | ); 56 | } catch (InvalidResponseException $e) { 57 | $errorCode = $e->getCode(); 58 | 59 | if (401 === $errorCode) { 60 | return Result::fromInvalidCredentials(); 61 | } 62 | 63 | throw $e; 64 | } 65 | 66 | if ($resultSet->isEmpty()) { 67 | throw InvalidResultException::fromEmptyResultSet(); 68 | } 69 | 70 | return Result::fromIdentity($identity); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Authentication/BlockCipherIdentityHandler.php: -------------------------------------------------------------------------------- 1 | blockCipher = $blockCipher; 18 | } 19 | 20 | public function createIdentity(string $username, string $password) : Identity 21 | { 22 | return new Identity( 23 | $username, 24 | $this->blockCipher->encrypt($password) 25 | ); 26 | } 27 | 28 | public function decryptPassword(Identity $identity) : string 29 | { 30 | return $this->blockCipher->decrypt($identity->getEncryptedPassword()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Authentication/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | username = $username; 21 | $this->encryptedPassword = $encryptedPassword; 22 | } 23 | 24 | public function getUsername() : string 25 | { 26 | return $this->username; 27 | } 28 | 29 | public function getEncryptedPassword() : string 30 | { 31 | return $this->encryptedPassword; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Authentication/IdentityHandlerInterface.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 18 | } 19 | 20 | public static function fromIdentity($identity) : self 21 | { 22 | return new self($identity); 23 | } 24 | 25 | public static function fromInvalidCredentials() : self 26 | { 27 | return new self(); 28 | } 29 | 30 | public function isSuccess() : bool 31 | { 32 | return null !== $this->identity; 33 | } 34 | 35 | public function getIdentity() : Identity 36 | { 37 | Assertion::notNull($this->identity); 38 | return $this->identity; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Client/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->type = $type; 29 | $this->valueList = $valueList; 30 | } 31 | 32 | public function getName() : string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function getType() : string 38 | { 39 | return $this->type; 40 | } 41 | 42 | public function hasValueList() : bool 43 | { 44 | return null !== $this->valueList; 45 | } 46 | 47 | public function getValueList() : ValueList 48 | { 49 | Assertion::notNull($this->valueList); 50 | return $this->valueList; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Client/Layout/Layout.php: -------------------------------------------------------------------------------- 1 | database = $database; 28 | $this->name = $name; 29 | $this->fields = $fields; 30 | } 31 | 32 | public function getDatabase() : string 33 | { 34 | return $this->database; 35 | } 36 | 37 | public function getName() : string 38 | { 39 | return $this->name; 40 | } 41 | 42 | /** 43 | * @return Field[] 44 | */ 45 | public function getFields() : array 46 | { 47 | return $this->fields; 48 | } 49 | 50 | public function hasField(string $name) : bool 51 | { 52 | return (bool) array_filter($this->fields, function (Field $field) use ($name) : bool { 53 | return $field->getName() === $name; 54 | }); 55 | } 56 | 57 | public function getField(string $name) : Field 58 | { 59 | $fields = array_filter($this->fields, function (Field $field) use ($name) : bool { 60 | return $field->getName() === $name; 61 | }); 62 | 63 | Assertion::notEmpty($fields); 64 | return reset($fields); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Client/Layout/LayoutClient.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 23 | } 24 | 25 | public function execute(Command $command) : Layout 26 | { 27 | $xml = $this->connection->execute($command, self::GRAMMAR_PATH); 28 | $errorCode = (int) $xml->ERRORCODE; 29 | 30 | if ($errorCode > 0) { 31 | throw FileMakerException::fromErrorCode($errorCode); 32 | } 33 | 34 | return new Layout( 35 | (string) $xml->LAYOUT['DATABASE'], 36 | (string) $xml->LAYOUT['NAME'], 37 | ...$this->parseFields($xml, $this->parseValueLists($xml)) 38 | ); 39 | } 40 | 41 | private function parseValueLists(SimpleXMLElement $xml) : array 42 | { 43 | $valueLists = []; 44 | 45 | foreach ($xml->VALUELISTS->VALUELIST as $valueList) { 46 | $values = []; 47 | 48 | foreach ($valueList->VALUE as $value) { 49 | $values[] = new Value((string) $value['DISPLAY'], (string) $value); 50 | } 51 | 52 | $valueLists[(string) $valueList['NAME']] = new ValueList((string) $valueList['NAME'], ...$values); 53 | } 54 | 55 | return $valueLists; 56 | } 57 | 58 | private function parseFields(SimpleXMLElement $xml, array $valueLists) : array 59 | { 60 | $fields = []; 61 | 62 | foreach ($xml->LAYOUT->FIELD as $field) { 63 | $valueListName = (string) $field->STYLE['VALUELIST']; 64 | 65 | $fields[] = new Field( 66 | (string) $field['NAME'], 67 | (string) $field->STYLE['TYPE'], 68 | ('' !== $valueListName ? $valueLists[$valueListName] : null) 69 | ); 70 | } 71 | 72 | return $fields; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Client/Layout/LayoutClientInterface.php: -------------------------------------------------------------------------------- 1 | display = $display; 21 | $this->value = $value; 22 | } 23 | 24 | public function getDisplay() : string 25 | { 26 | return $this->display; 27 | } 28 | 29 | public function getValue() : string 30 | { 31 | return $this->value; 32 | } 33 | 34 | public function __toString() : string 35 | { 36 | return $this->display; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Client/Layout/ValueList.php: -------------------------------------------------------------------------------- 1 | name = $name; 21 | $this->values = $values; 22 | } 23 | 24 | public function getName() : string 25 | { 26 | return $this->name; 27 | } 28 | 29 | /** 30 | * @return Value[] 31 | */ 32 | public function getValues() : array 33 | { 34 | return $this->values; 35 | } 36 | 37 | public function __toString() : string 38 | { 39 | return $this->name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Client/ResultSet/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getMessage() 28 | ), 0, $previousException); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Client/ResultSet/Exception/UnknownFieldException.php: -------------------------------------------------------------------------------- 1 | getMessage() 31 | ), 0, $previousException); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Client/ResultSet/ResultSetClientInterface.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 18 | } 19 | 20 | public function __invoke(string $value) 21 | { 22 | if ('' === $value) { 23 | return null; 24 | } 25 | 26 | return new StreamProxy($this->connection, $value); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Client/ResultSet/Transformer/DateTimeTransformer.php: -------------------------------------------------------------------------------- 1 | timeZone = $timeZone; 20 | } 21 | 22 | public function __invoke(string $value) 23 | { 24 | if ('' === $value) { 25 | return null; 26 | } 27 | 28 | $dateTime = DateTimeImmutable::createFromFormat('!m/d/Y H:i:s', $value, $this->timeZone); 29 | 30 | if (false === $dateTime) { 31 | throw DateTimeException::fromDateTimeError($value, DateTimeImmutable::getLastErrors()); 32 | } 33 | 34 | return $dateTime; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Client/ResultSet/Transformer/DateTransformer.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 29 | $this->assetUri = $assetUri; 30 | } 31 | 32 | public function __toString() : string 33 | { 34 | return $this->getWrappedStream()->__toString(); 35 | } 36 | 37 | public function close() 38 | { 39 | $this->getWrappedStream()->close(); 40 | } 41 | 42 | public function detach() 43 | { 44 | return $this->getWrappedStream()->detach(); 45 | } 46 | 47 | public function eof() : bool 48 | { 49 | return $this->getWrappedStream()->eof(); 50 | } 51 | 52 | public function getContents() : string 53 | { 54 | return $this->getWrappedStream()->getContents(); 55 | } 56 | 57 | public function getMetadata($key = null) 58 | { 59 | return $this->getWrappedStream()->getMetadata($key); 60 | } 61 | 62 | public function getSize() 63 | { 64 | return $this->getWrappedStream()->getSize(); 65 | } 66 | 67 | public function isReadable() : bool 68 | { 69 | return $this->getWrappedStream()->isReadable(); 70 | } 71 | 72 | public function isSeekable() : bool 73 | { 74 | return $this->getWrappedStream()->isSeekable(); 75 | } 76 | 77 | public function isWritable() : bool 78 | { 79 | return $this->getWrappedStream()->isWritable(); 80 | } 81 | 82 | public function read($length) : string 83 | { 84 | return $this->getWrappedStream()->read($length); 85 | } 86 | 87 | public function rewind() 88 | { 89 | $this->getWrappedStream()->rewind(); 90 | } 91 | 92 | public function seek($offset, $whence = SEEK_SET) 93 | { 94 | $this->getWrappedStream()->seek($offset, $whence); 95 | } 96 | 97 | public function tell() : int 98 | { 99 | return $this->getWrappedStream()->tell(); 100 | } 101 | 102 | public function write($string) : int 103 | { 104 | return $this->getWrappedStream()->write($string); 105 | } 106 | 107 | private function getWrappedStream() : StreamInterface 108 | { 109 | if (null !== $this->wrappedStream) { 110 | return $this->wrappedStream; 111 | } 112 | 113 | return $this->wrappedStream = $this->connection->getAsset($this->assetUri); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Client/ResultSet/Transformer/TextTransformer.php: -------------------------------------------------------------------------------- 1 | items = new ArrayIterator($items); 25 | $this->totalCount = $totalCount; 26 | } 27 | 28 | public function count() : int 29 | { 30 | return count($this->items); 31 | } 32 | 33 | public function getTotalCount() : int 34 | { 35 | return $this->totalCount; 36 | } 37 | 38 | public function isEmpty() : bool 39 | { 40 | return 0 === count($this->items); 41 | } 42 | 43 | public function first() 44 | { 45 | if ($this->isEmpty()) { 46 | return null; 47 | } 48 | 49 | $this->items->rewind(); 50 | return $this->items->current(); 51 | } 52 | 53 | public function getIterator() : Traversable 54 | { 55 | return $this->items; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Connection/Command.php: -------------------------------------------------------------------------------- 1 | parameters = ['-lay' => $layout] + $parameters; 45 | } 46 | 47 | public function withIdentity(Identity $identity) : self 48 | { 49 | $command = clone $this; 50 | $command->identity = $identity; 51 | 52 | return $command; 53 | } 54 | 55 | public function hasIdentity() 56 | { 57 | return null !== $this->identity; 58 | } 59 | 60 | public function getIdentity() : Identity 61 | { 62 | Assertion::notNull($this->identity); 63 | return $this->identity; 64 | } 65 | 66 | public function getLayout() : string 67 | { 68 | return $this->parameters['-lay']; 69 | } 70 | 71 | public function __toString() : string 72 | { 73 | $parts = []; 74 | 75 | foreach ($this->parameters as $name => $value) { 76 | if ($value instanceof DateTimeInterface) { 77 | $value = $value->format('m/d/Y H:i:s'); 78 | } 79 | 80 | if ($value instanceof Decimal) { 81 | $value = (string) $value; 82 | } 83 | 84 | if (null === $value || '' === $value) { 85 | $parts[] = urlencode((string) $name); 86 | continue; 87 | } 88 | 89 | $parts[] = sprintf('%s=%s', urlencode((string) $name), urlencode((string) $value)); 90 | } 91 | 92 | return implode('&', $parts); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Connection/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), 17 | $response->getReasonPhrase() 18 | ), (int) $response->getStatusCode()); 19 | } 20 | 21 | public static function fromXmlError(LibXMLError $error) : self 22 | { 23 | return new self(sprintf('An unexpected XML error occured: %s', $error->message)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/Builder/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getPropertyName(), 21 | $entityMetadata->getClassName(), 22 | $previousException->getMessage() 23 | ), 0, $previousException); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/Builder/Exception/HydrationException.php: -------------------------------------------------------------------------------- 1 | getPropertyName(), 21 | $entityMetadata->getClassName(), 22 | $previousException->getMessage() 23 | ), 0, $previousException); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/Embeddable.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 29 | $this->fieldNamePrefix = $fieldNamePrefix; 30 | $this->metadata = $metadata; 31 | } 32 | 33 | public function getPropertyName() : string 34 | { 35 | return $this->propertyName; 36 | } 37 | 38 | public function getFieldNamePrefix() : string 39 | { 40 | return $this->fieldNamePrefix; 41 | } 42 | 43 | public function getMetadata() : Entity 44 | { 45 | return $this->metadata; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/Entity.php: -------------------------------------------------------------------------------- 1 | validateArray($fields, Field::class); 67 | $this->validateArray($embeddables, Embeddable::class); 68 | $this->validateArray($oneToMany, OneToMany::class); 69 | $this->validateArray($manyToOne, ManyToOne::class); 70 | $this->validateArray($oneToOne, OneToOne::class); 71 | 72 | $this->layout = $layout; 73 | $this->className = $className; 74 | $this->fields = $fields; 75 | $this->embeddables = $embeddables; 76 | $this->oneToMany = $oneToMany; 77 | $this->manyToOne = $manyToOne; 78 | $this->oneToOne = $oneToOne; 79 | $this->recordId = $recordId; 80 | $this->interfaceName = $interfaceName; 81 | } 82 | 83 | public function getLayout() : string 84 | { 85 | return $this->layout; 86 | } 87 | 88 | public function getClassName() : string 89 | { 90 | return $this->className; 91 | } 92 | 93 | public function hasInterfaceName() : bool 94 | { 95 | return null !== $this->interfaceName; 96 | } 97 | 98 | public function getInterfaceName() : string 99 | { 100 | Assertion::notNull($this->interfaceName); 101 | return $this->interfaceName; 102 | } 103 | 104 | /** 105 | * @return Field[] 106 | */ 107 | public function getFields() : array 108 | { 109 | return $this->fields; 110 | } 111 | 112 | /** 113 | * @return Embeddable[] 114 | */ 115 | public function getEmbeddables() : array 116 | { 117 | return $this->embeddables; 118 | } 119 | 120 | /** 121 | * @return OneToMany[] 122 | */ 123 | public function getOneToMany() : array 124 | { 125 | return $this->oneToMany; 126 | } 127 | 128 | /** 129 | * @return ManyToOne[] 130 | */ 131 | public function getManyToOne() : array 132 | { 133 | return $this->manyToOne; 134 | } 135 | 136 | /** 137 | * @return OneToOne[] 138 | */ 139 | public function getOneToOne() : array 140 | { 141 | return $this->oneToOne; 142 | } 143 | 144 | public function hasRecordId() : bool 145 | { 146 | return null !== $this->recordId; 147 | } 148 | 149 | public function getRecordId() : RecordId 150 | { 151 | Assertion::notNull($this->recordId); 152 | return $this->recordId; 153 | } 154 | 155 | private function validateArray(array $array, string $expectedClassName) 156 | { 157 | Assertion::count(array_filter($array, function ($metadata) use ($expectedClassName) : bool { 158 | return !$metadata instanceof $expectedClassName; 159 | }), 0, sprintf('At least one element in array is not an instance of %s', $expectedClassName)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 43 | $this->propertyName = $propertyName; 44 | $this->type = $type; 45 | $this->repeatable = $repeatable; 46 | $this->readOnly = $readOnly; 47 | } 48 | 49 | public function getFieldName() : string 50 | { 51 | return $this->fieldName; 52 | } 53 | 54 | public function getPropertyName() : string 55 | { 56 | return $this->propertyName; 57 | } 58 | 59 | public function getType() : TypeInterface 60 | { 61 | return $this->type; 62 | } 63 | 64 | public function isRepeatable() : bool 65 | { 66 | return $this->repeatable; 67 | } 68 | 69 | public function isReadOnly() : bool 70 | { 71 | return $this->readOnly; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/ManyToOne.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 67 | $this->propertyName = $propertyName; 68 | $this->targetTable = $targetTable; 69 | $this->targetEntity = $targetEntity; 70 | $this->targetPropertyName = $targetPropertyName; 71 | $this->targetFieldName = $targetFieldName; 72 | $this->targetInterfaceName = $targetInterfaceName; 73 | $this->readOnly = $readOnly; 74 | $this->eagerHydration = $eagerHydration; 75 | } 76 | 77 | public function getFieldName() : string 78 | { 79 | return $this->fieldName; 80 | } 81 | 82 | public function getPropertyName() : string 83 | { 84 | return $this->propertyName; 85 | } 86 | 87 | public function getTargetTable() : string 88 | { 89 | return $this->targetTable; 90 | } 91 | 92 | public function getTargetEntity() : string 93 | { 94 | return $this->targetEntity; 95 | } 96 | 97 | public function getTargetPropertyName() : string 98 | { 99 | return $this->targetPropertyName; 100 | } 101 | 102 | public function getTargetFieldName() : string 103 | { 104 | return $this->targetFieldName; 105 | } 106 | 107 | public function getTargetInterfaceName() : string 108 | { 109 | Assertion::notNull( 110 | $this->targetInterfaceName, 111 | sprintf('Target entity %s has no interface name defined', $this->targetEntity) 112 | ); 113 | return $this->targetInterfaceName; 114 | } 115 | 116 | public function isReadOnly() : bool 117 | { 118 | return $this->readOnly; 119 | } 120 | 121 | public function hasEagerHydration() : bool 122 | { 123 | return $this->eagerHydration; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/MetadataBuilderInterface.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 41 | $this->targetTable = $targetTable; 42 | $this->targetEntity = $targetEntity; 43 | $this->targetFieldName = $targetFieldName; 44 | $this->eagerHydration = $eagerHydration; 45 | } 46 | 47 | public function getPropertyName() : string 48 | { 49 | return $this->propertyName; 50 | } 51 | 52 | public function getTargetTable() : string 53 | { 54 | return $this->targetTable; 55 | } 56 | 57 | public function getTargetEntity() : string 58 | { 59 | return $this->targetEntity; 60 | } 61 | 62 | public function getTargetFieldName() : string 63 | { 64 | return $this->targetFieldName; 65 | } 66 | 67 | public function hasEagerHydration() : bool 68 | { 69 | return $this->eagerHydration; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/OneToOne.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 78 | $this->targetTable = $targetTable; 79 | $this->targetEntity = $targetEntity; 80 | $this->targetFieldName = $targetFieldName; 81 | $this->targetInterfaceName = $targetInterfaceName; 82 | $this->owningSide = $owningSide; 83 | $this->readOnly = $owningSide ? $readOnly : true; 84 | $this->fieldName = $owningSide ? $fieldName : null; 85 | $this->targetPropertyName = $owningSide ? $targetPropertyName : null; 86 | $this->eagerHydration = $eagerHydration; 87 | } 88 | 89 | public function getPropertyName() : string 90 | { 91 | return $this->propertyName; 92 | } 93 | 94 | public function getTargetTable() : string 95 | { 96 | return $this->targetTable; 97 | } 98 | 99 | public function getTargetEntity() : string 100 | { 101 | return $this->targetEntity; 102 | } 103 | 104 | public function getTargetFieldName() : string 105 | { 106 | return $this->targetFieldName; 107 | } 108 | 109 | public function getTargetInterfaceName() : string 110 | { 111 | Assertion::notNull( 112 | $this->targetInterfaceName, 113 | sprintf('Target entity %s has no interface name defined', $this->targetEntity) 114 | ); 115 | return $this->targetInterfaceName; 116 | } 117 | 118 | public function isOwningSide() : bool 119 | { 120 | return $this->owningSide; 121 | } 122 | 123 | public function getFieldName() : string 124 | { 125 | Assertion::notNull($this->fieldName); 126 | return $this->fieldName; 127 | } 128 | 129 | public function getTargetPropertyName() : string 130 | { 131 | Assertion::notNull($this->targetPropertyName); 132 | return $this->targetPropertyName; 133 | } 134 | 135 | public function isReadOnly() : bool 136 | { 137 | return $this->readOnly; 138 | } 139 | 140 | public function hasEagerHydration() : bool 141 | { 142 | return $this->eagerHydration; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Repository/Builder/Metadata/RecordId.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 16 | } 17 | 18 | public function getPropertyName() : string 19 | { 20 | return $this->propertyName; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Repository/Builder/Proxy/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | resultSetClient = $resultSetClient; 40 | $this->metadataBuilder = $metadataBuilder; 41 | $this->proxyBuilder = $proxyBuilder; 42 | } 43 | 44 | public function buildRepository(string $entityClassName) : RepositoryInterface 45 | { 46 | if (array_key_exists($entityClassName, $this->repositories)) { 47 | return $this->repositories[$entityClassName]; 48 | } 49 | 50 | $metadata = $this->metadataBuilder->getMetadata($entityClassName); 51 | 52 | return ($this->repositories[$entityClassName] = new Repository( 53 | $this->resultSetClient, 54 | $metadata->getLayout(), 55 | new MetadataHydration($this, $this->proxyBuilder, $metadata), 56 | new MetadataExtraction($metadata) 57 | )); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Repository/Builder/RepositoryBuilderInterface.php: -------------------------------------------------------------------------------- 1 | comp(Decimal::fromInteger(0)); 19 | } 20 | 21 | if (is_string($value)) { 22 | return $value !== '0' && $value !== ''; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | public function toFileMakerValue($value) 29 | { 30 | Assertion::boolean($value); 31 | return Decimal::fromInteger($value ? 1 : 0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Repository/Builder/Type/DateTimeType.php: -------------------------------------------------------------------------------- 1 | format('m/d/Y'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/Builder/Type/DecimalType.php: -------------------------------------------------------------------------------- 1 | asFloat(); 19 | } 20 | 21 | public function toFileMakerValue($value) 22 | { 23 | if (null === $value) { 24 | return null; 25 | } 26 | 27 | Assertion::float($value); 28 | return Decimal::fromFloat($value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/Builder/Type/IntegerType.php: -------------------------------------------------------------------------------- 1 | asInteger(); 19 | } 20 | 21 | public function toFileMakerValue($value) 22 | { 23 | if (null === $value) { 24 | return null; 25 | } 26 | 27 | Assertion::integer($value); 28 | return Decimal::fromInteger($value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/Builder/Type/NullableStringType.php: -------------------------------------------------------------------------------- 1 | format('H:i:s'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/Builder/Type/TypeInterface.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 39 | $this->idFieldName = $idFieldName; 40 | $this->sparseRecords = $sparseRecords; 41 | } 42 | 43 | public function getIterator() : Traversable 44 | { 45 | if (null !== $this->iterator) { 46 | return $this->iterator; 47 | } 48 | 49 | if (empty($this->sparseRecords)) { 50 | return $this->iterator = new ArrayIterator(); 51 | } 52 | 53 | $findQuery = new FindQuery(); 54 | $findQuery->addOrQueries(...array_map(function (array $sparseRecord) : Query { 55 | return new Query($this->idFieldName, (string) $sparseRecord[$this->idFieldName]); 56 | }, $this->sparseRecords)); 57 | 58 | return $this->iterator = new IteratorIterator($this->repository->findByQuery($findQuery)); 59 | } 60 | 61 | public function count() : int 62 | { 63 | return count($this->sparseRecords); 64 | } 65 | 66 | public function getTotalCount() : int 67 | { 68 | return count($this->sparseRecords); 69 | } 70 | 71 | public function isEmpty() : bool 72 | { 73 | return 0 === count($this->sparseRecords); 74 | } 75 | 76 | public function first() 77 | { 78 | if ($this->isEmpty()) { 79 | return null; 80 | } 81 | 82 | $iterator = $this->getIterator(); 83 | $iterator->rewind(); 84 | return $iterator->current(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Repository/Query/Exception/EmptyQueryException.php: -------------------------------------------------------------------------------- 1 | queries += $queries; 23 | } 24 | 25 | public function addAndQueries(Query ...$queries) 26 | { 27 | if (empty($queries)) { 28 | throw InvalidArgumentException::fromEmptyQueryParameters(); 29 | } 30 | 31 | $this->queries[] = $queries; 32 | } 33 | 34 | public function toParameters() : array 35 | { 36 | if (empty($this->queries)) { 37 | throw EmptyQueryException::fromEmptyQueryArray(); 38 | } 39 | 40 | $parameters = [ 41 | '-query' => $this->buildQueryParameter(), 42 | ]; 43 | 44 | $index = 0; 45 | 46 | foreach ($this->queries as $query) { 47 | if ($query instanceof Query) { 48 | $parameters[sprintf('-q%d', ++$index)] = $query->getFieldName(); 49 | $parameters[sprintf('-q%d.value', $index)] = $query->getValue(); 50 | continue; 51 | } 52 | 53 | foreach ($query as $andQuery) { 54 | $parameters[sprintf('-q%d', ++$index)] = $andQuery->getFieldName(); 55 | $parameters[sprintf('-q%d.value', $index)] = $andQuery->getValue(); 56 | continue; 57 | } 58 | } 59 | 60 | return $parameters; 61 | } 62 | 63 | private function buildQueryParameter() : string 64 | { 65 | $index = 0; 66 | $orQueries = []; 67 | 68 | foreach ($this->queries as $query) { 69 | if ($query instanceof Query) { 70 | $orQueries[] = sprintf('%sq%d', $query->isExclude() ? '!' : '', ++$index); 71 | continue; 72 | } 73 | 74 | $andQueries = []; 75 | 76 | foreach ($query as $andQuery) { 77 | $andQueries[] = sprintf('%sq%d', $andQuery->isExclude() ? '!' : '', ++$index); 78 | } 79 | 80 | $orQueries[] = implode(',', $andQueries); 81 | } 82 | 83 | return '(' . implode(');(', $orQueries) . ')'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Repository/Query/Query.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 26 | $this->value = $value; 27 | $this->exclude = $exclude; 28 | } 29 | 30 | public function getFieldName() : string 31 | { 32 | return $this->fieldName; 33 | } 34 | 35 | public function getValue() : string 36 | { 37 | return $this->value; 38 | } 39 | 40 | public function isExclude() : bool 41 | { 42 | return $this->exclude; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Repository/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | createResultSetClientProphecy(); 24 | $resultSetClient->execute( 25 | $this->createCommand('foo')->withIdentity($identity) 26 | )->willReturn(new ItemCollection([[]], 1)); 27 | 28 | $authenticator = $this->createAuthenticator($resultSetClient->reveal(), $identity); 29 | 30 | $this->assertSame($identity, $authenticator->authenticate('foo', 'bar')->getIdentity()); 31 | } 32 | 33 | public function testAuthenticatorGenericFail() 34 | { 35 | $identity = new Identity('foo', 'bar'); 36 | 37 | $resultSetClient = $this->createResultSetClientProphecy(); 38 | $resultSetClient->execute( 39 | $this->createCommand('foo')->withIdentity($identity) 40 | )->willThrow(InvalidResponseException::class); 41 | 42 | $authenticator = $this->createAuthenticator($resultSetClient->reveal(), $identity); 43 | 44 | $this->expectException(InvalidResponseException::class); 45 | $authenticator->authenticate('foo', 'bar')->getIdentity(); 46 | } 47 | 48 | public function testAuthenticator401NotFound() 49 | { 50 | $identity = new Identity('foo', 'bar'); 51 | 52 | $response = $this->prophesize(ResponseInterface::class); 53 | $response->getStatusCode()->willReturn(401); 54 | $response->getReasonPhrase()->willReturn('Not Found'); 55 | 56 | $resultSetClient = $this->createResultSetClientProphecy(); 57 | $resultSetClient->execute( 58 | $this->createCommand('foo')->withIdentity($identity) 59 | )->willThrow(InvalidResponseException::fromUnsuccessfulResponse($response->reveal())); 60 | 61 | $authenticator = $this->createAuthenticator($resultSetClient->reveal(), $identity); 62 | $this->assertFalse($authenticator->authenticate('foo', 'bar')->isSuccess()); 63 | } 64 | 65 | public function testAuthenticatorEmptyResultFail() 66 | { 67 | $identity = new Identity('foo', 'bar'); 68 | $resultSetClient = $this->createResultSetClientProphecy(); 69 | $resultSetClient->execute( 70 | $this->createCommand('foo')->withIdentity($identity) 71 | )->willReturn(new ItemCollection([], 0)); 72 | 73 | $authenticator = $this->createAuthenticator($resultSetClient->reveal(), $identity); 74 | 75 | $this->expectException(InvalidResultException::class); 76 | $authenticator->authenticate('foo', 'bar')->getIdentity(); 77 | } 78 | 79 | private function createResultSetClientProphecy() : ObjectProphecy 80 | { 81 | $resultSetClient = $this->prophesize(ResultSetClientInterface::class); 82 | $resultSetClient->quoteString(\Prophecy\Argument::any())->will(function (array $parameters) : string { 83 | return $parameters[0]; 84 | }); 85 | 86 | return $resultSetClient; 87 | } 88 | 89 | private function createAuthenticator(ResultSetClientInterface $resultSetClient, Identity $identity) : Authenticator 90 | { 91 | $identityHandler = $this->prophesize(IdentityHandlerInterface::class); 92 | $identityHandler->createIdentity('foo', 'bar')->willReturn($identity); 93 | 94 | return new Authenticator( 95 | $resultSetClient, 96 | $identityHandler->reveal(), 97 | 'layout', 98 | 'account' 99 | ); 100 | } 101 | 102 | private function createCommand(string $username) : Command 103 | { 104 | return new Command('layout', ['account' => '==' . $username, '-find' => null]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/Authentication/BlockCipherIdentityHandlerTest.php: -------------------------------------------------------------------------------- 1 | prophesize(BlockCipher::class); 16 | $blockCipher->encrypt('bar')->willReturn('baz'); 17 | 18 | $identityHandler = new BlockCipherIdentityHandler($blockCipher->reveal()); 19 | $identity = $identityHandler->createIdentity('foo', 'bar'); 20 | 21 | $this->assertSame('foo', $identity->getUsername()); 22 | $this->assertSame('baz', $identity->getEncryptedPassword()); 23 | } 24 | 25 | public function testDecryptPassword() 26 | { 27 | $blockCipher = $this->prophesize(BlockCipher::class); 28 | $blockCipher->decrypt('baz')->willReturn('bar'); 29 | 30 | $identityHandler = new BlockCipherIdentityHandler($blockCipher->reveal()); 31 | $this->assertSame('bar', $identityHandler->decryptPassword(new Identity('foo', 'baz'))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/Authentication/IdentityTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', $identity->getUsername()); 15 | $this->assertSame('bar', $identity->getEncryptedPassword()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Authentication/ResultTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($result->isSuccess()); 19 | $this->assertSame($identity, $result->getIdentity()); 20 | } 21 | 22 | public function testUnsuccessfulResult() 23 | { 24 | $result = Result::fromInvalidCredentials(); 25 | 26 | $this->assertFalse($result->isSuccess()); 27 | $this->expectException(InvalidArgumentException::class); 28 | $result->getIdentity(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/Client/Layout/LayoutClientTest.php: -------------------------------------------------------------------------------- 1 | prophesize(ConnectionInterface::class); 20 | $connection->execute($command, '/fmi/xml/FMPXMLLAYOUT.xml')->willReturn($xml); 21 | $client = new LayoutClient($connection->reveal()); 22 | 23 | $layout = $client->execute($command); 24 | 25 | $this->assertSame('FMServer_Sample', $layout->getDatabase()); 26 | $this->assertSame('Projects | Web', $layout->getName()); 27 | $this->assertCount(9, $layout->getFields()); 28 | 29 | $this->assertTrue($layout->hasField('Projects::Project Name')); 30 | $nameField = $layout->getField('Projects::Project Name'); 31 | $this->assertFalse($nameField->hasValueList()); 32 | $this->assertSame('EDITTEXT', $nameField->getType()); 33 | 34 | $this->assertTrue($layout->hasField('Projects::Status on Screen')); 35 | $statusField = $layout->getField('Projects::Status on Screen'); 36 | $this->assertTrue($statusField->hasValueList()); 37 | $this->assertSame('Status', (string) $statusField->getValueList()); 38 | $this->assertSame('Status', $statusField->getValueList()->getName()); 39 | $this->assertSame('Completed', (string) $statusField->getValueList()->getValues()[0]); 40 | $this->assertSame('Completed', $statusField->getValueList()->getValues()[0]->getDisplay()); 41 | $this->assertSame('Completed', $statusField->getValueList()->getValues()[0]->getValue()); 42 | 43 | $this->expectException(InvalidArgumentException::class); 44 | $layout->getField('non-existent'); 45 | } 46 | 47 | public function testErrorXml() 48 | { 49 | $command = new Command('foo', []); 50 | $xml = simplexml_load_file(__DIR__ . '/TestAssets/error_fmpxmllayout.xml'); 51 | $connection = $this->prophesize(ConnectionInterface::class); 52 | $connection->execute($command, '/fmi/xml/FMPXMLLAYOUT.xml')->willReturn($xml); 53 | $client = new LayoutClient($connection->reveal()); 54 | 55 | $this->expectException(FileMakerException::class); 56 | $this->expectExceptionMessage('User canceled action'); 57 | $this->expectExceptionCode(1); 58 | $client->execute($command); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/Client/Layout/TestAssets/error_fmpxmllayout.xml: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /test/Client/Layout/TestAssets/sample_fmpxmllayout.xml: -------------------------------------------------------------------------------- 1 | 0CompletedDueIn ProgressOverduePendingfinancemanufacturingTim Thomson -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/ParentChildAssociations/Base_sparse-collection.xml: -------------------------------------------------------------------------------- 1 | 2210000000000000000000000000000000000Some other text with line breaks 2 | Line two 3 | Line three3258.72771254957897713067Some text with line breaks 4 | Line two 5 | Line three -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/deleted-field.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/duplicateportals.xml: -------------------------------------------------------------------------------- 1 | jmedemajmedema05/01/2008 06:27:2305/01/2008 06:27:23ApplicationVersion: Web Publishing Engine 8.0v4 2 | HostApplicationVersion: ? 3 | PrivilegeSetName: [Full Access] 4 | SystemPlatform: -2 5 | SystemVersion: 5.2240BFAB59CB-61F9-4803-9A91-000D608435B8BFAB59CB-61F9-4803-9A91-000D608435B8BFAB59CB-61F9-4803-9A91-000D608435B8BFAB59CB-61F9-4803-9A91-000D608435B8240 -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PHP: Behind the Parser 5 | 6 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypeDate.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 09/14/2016 23 | 24 | 25 | 26 | 27 | 28 | invalid date value 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypeFake.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 94.82918507895723447869 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypeRepeatingNumber.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 1 17 | 1.1 18 | 0 19 | -1 20 | 21 | 22 | 23 | 24 | 25 | non-number string 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypeTime.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 14:22:23 23 | 24 | 25 | 26 | 27 | 28 | invalid time value 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypeTimestamp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 09/14/2016 14:22:23 23 | 24 | 25 | 26 | 27 | 28 | invalid timestamp value 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/invalidFieldTypes.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 14 | 17 | 18 | 19 | 20 | 21 | 94.82918507895723447869 22 | 23 | 24 | invalid data 09/14/2016 14:22:23 25 | 26 | 27 | wrong 28 | scalar 29 | type 30 | 31 | gaps 32 | 33 | 34 | 35 | 36 | last one 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/reservedfieldname.xml: -------------------------------------------------------------------------------- 1 | rjacquesrjacques12/04/2008 19:10:3712/04/2008 19:10:37ApplicationVersion: Web Publishing Engine 8.0v4 2 | HostApplicationVersion: ? 3 | PrivilegeSetName: web 4 | SystemPlatform: -2 5 | SystemVersion: 5.2 -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/sample_fmresultset_empty.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Client/ResultSet/TestAssets/sample_fmresultset_fmerror4.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Client/ResultSet/Type/NumberTransformerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($numberTransformer($fileMakerValue), Decimal::fromString($expectedValue)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Client/ResultSet/Type/StreamProxyTest.php: -------------------------------------------------------------------------------- 1 | prophesize(StreamInterface::class); 40 | $stream->{$methodName}(...$arguments)->willReturn($returnValue)->shouldBeCalled(); 41 | 42 | $connection = $this->prophesize(ConnectionInterface::class); 43 | $connection->getAsset('/foo')->willReturn($stream->reveal()); 44 | 45 | $proxy = new StreamProxy($connection->reveal(), '/foo'); 46 | $this->assertSame($returnValue, $proxy->{$methodName}(...$arguments)); 47 | } 48 | 49 | public function testWrappedStreamIsOnlyRetrievedOnce() 50 | { 51 | $connection = $this->prophesize(ConnectionInterface::class); 52 | $connection 53 | ->getAsset('/foo') 54 | ->shouldBeCalledTimes(1) 55 | ->willReturn($this->prophesize(StreamInterface::class)->reveal()); 56 | 57 | $proxy = new StreamProxy($connection->reveal(), '/foo'); 58 | $proxy->rewind(); 59 | $proxy->rewind(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/Collection/ItemCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($collection->isEmpty()); 15 | $this->assertSame(['foo', 'bar'], iterator_to_array($collection->getIterator())); 16 | } 17 | 18 | public function testEmptyCollection() 19 | { 20 | $collection = new ItemCollection([], 0); 21 | $this->assertTrue($collection->isEmpty()); 22 | $this->assertNull($collection->first()); 23 | } 24 | 25 | public function testFirst() 26 | { 27 | $collection = new ItemCollection(['foo', 'bar'], 2); 28 | $this->assertSame('foo', $collection->first()); 29 | } 30 | 31 | public function testCount() 32 | { 33 | $collection = new ItemCollection(['foo', 'bar'], 4); 34 | $this->assertSame(2, count($collection)); 35 | } 36 | 37 | public function testGetTotalCount() 38 | { 39 | $collection = new ItemCollection(['foo', 'bar'], 4); 40 | $this->assertSame(4, $collection->getTotalCount()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Connection/CommandTest.php: -------------------------------------------------------------------------------- 1 | expectException(DomainException::class); 19 | $this->expectExceptionMessage('parameter "-db" is not allowed'); 20 | new Command('foo', ['-db' => 'foo']); 21 | } 22 | 23 | public function testUseDisallowedLayoutParameter() 24 | { 25 | $this->expectException(DomainException::class); 26 | $this->expectExceptionMessage('parameter "-lay" is not allowed'); 27 | new Command('foo', ['-lay' => 'foo']); 28 | } 29 | 30 | public function testUseDisallowedValueType() 31 | { 32 | $this->expectException(DomainException::class); 33 | $this->expectExceptionMessage('must either be scalar, null, Decimal or implement DateTimeInterface'); 34 | new Command('foo', ['bar' => []]); 35 | } 36 | 37 | public function testGetLayout() 38 | { 39 | $command = new Command('foo', []); 40 | $this->assertSame('foo', $command->getLayout()); 41 | } 42 | 43 | public function testCloneWithIdentity() 44 | { 45 | $command = new Command('foo', []); 46 | $identity = new Identity('foo', 'bar'); 47 | $newCommand = $command->withIdentity($identity); 48 | 49 | $this->assertNotSame($newCommand, $command); 50 | $this->assertFalse($command->hasIdentity()); 51 | $this->assertTrue($newCommand->hasIdentity()); 52 | $this->assertSame($identity, $newCommand->getIdentity()); 53 | } 54 | 55 | public function testGetIdentityWithoutIdentity() 56 | { 57 | $this->expectException(InvalidArgumentException::class); 58 | $command = new Command('foo', []); 59 | $command->getIdentity(); 60 | } 61 | 62 | public function parameterProvider() : array 63 | { 64 | return [ 65 | 'no-parameters' => [ 66 | [], 67 | '-lay=foo', 68 | ], 69 | 'empty-parameter' => [ 70 | ['foo' => ''], 71 | '-lay=foo&foo', 72 | ], 73 | 'null-parameter' => [ 74 | ['foo' => null], 75 | '-lay=foo&foo', 76 | ], 77 | 'string-parameter' => [ 78 | ['foo' => 'bar'], 79 | '-lay=foo&foo=bar', 80 | ], 81 | 'integer-parameter' => [ 82 | ['foo' => 3], 83 | '-lay=foo&foo=3', 84 | ], 85 | 'float-parameter' => [ 86 | ['foo' => 3.3], 87 | '-lay=foo&foo=3.3', 88 | ], 89 | 'boolean-parameter' => [ 90 | ['foo' => true], 91 | '-lay=foo&foo=1', 92 | ], 93 | 'datetime-parameter' => [ 94 | ['foo' => new DateTimeImmutable('2016-01-01 00:00:00 UTC')], 95 | '-lay=foo&foo=01%2F01%2F2016+00%3A00%3A00', 96 | ], 97 | 'decimal-parameter' => [ 98 | ['foo' => Decimal::fromString('12.499734362638823')], 99 | '-lay=foo&foo=12.499734362638823', 100 | ], 101 | ]; 102 | } 103 | 104 | /** 105 | * @dataProvider parameterProvider 106 | */ 107 | public function testToString(array $parameters, string $expectedString) 108 | { 109 | $command = new Command('foo', $parameters); 110 | $this->assertSame($expectedString, (string) $command); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/EmbeddableTest.php: -------------------------------------------------------------------------------- 1 | assertSame('propertyName', $metadata->getPropertyName()); 18 | $this->assertSame('fieldNamePrefix', $metadata->getFieldNamePrefix()); 19 | $this->assertSame($entityMetadata, $metadata->getMetadata()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/FieldTest.php: -------------------------------------------------------------------------------- 1 | prophesize(TypeInterface::class)->reveal(); 15 | 16 | $metadata = new Field('fieldName', 'propertyName', $type, true, true); 17 | $this->assertSame('fieldName', $metadata->getFieldName()); 18 | $this->assertSame('propertyName', $metadata->getPropertyName()); 19 | $this->assertSame($type, $metadata->getType()); 20 | $this->assertTrue($metadata->isRepeatable()); 21 | $this->assertTrue($metadata->isReadOnly()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/ManyToOneTest.php: -------------------------------------------------------------------------------- 1 | assertSame('fieldName', $metadata->getFieldName()); 24 | $this->assertSame('propertyName', $metadata->getPropertyName()); 25 | $this->assertSame('targetTable', $metadata->getTargetTable()); 26 | $this->assertSame('targetEntity', $metadata->getTargetEntity()); 27 | $this->assertSame('targetPropertyName', $metadata->getTargetPropertyName()); 28 | $this->assertSame('targetFieldName', $metadata->getTargetFieldName()); 29 | $this->assertSame('targetInterfaceName', $metadata->getTargetInterfaceName()); 30 | $this->assertTrue($metadata->isReadOnly()); 31 | $this->assertFalse($metadata->hasEagerHydration()); 32 | } 33 | 34 | public function testSettingEagerHydration() 35 | { 36 | $metadata = new ManyToOne('', '', '', '', '', '', '', false, true); 37 | $this->assertTrue($metadata->hasEagerHydration()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/OneToManyTest.php: -------------------------------------------------------------------------------- 1 | assertSame('propertyName', $metadata->getPropertyName()); 15 | $this->assertSame('targetTable', $metadata->getTargetTable()); 16 | $this->assertSame('targetEntity', $metadata->getTargetEntity()); 17 | $this->assertSame('targetFieldName', $metadata->getTargetFieldName()); 18 | $this->assertFalse($metadata->hasEagerHydration()); 19 | } 20 | 21 | public function testSettingEagerHydration() 22 | { 23 | $metadata = new OneToMany('', '', '', '', true); 24 | $this->assertTrue($metadata->hasEagerHydration()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/OneToOneTest.php: -------------------------------------------------------------------------------- 1 | assertSame('propertyName', $metadata->getPropertyName()); 26 | $this->assertSame('targetTable', $metadata->getTargetTable()); 27 | $this->assertSame('targetEntity', $metadata->getTargetEntity()); 28 | $this->assertSame('targetInterfaceName', $metadata->getTargetInterfaceName()); 29 | $this->assertTrue($metadata->isOwningSide()); 30 | $this->assertTrue($metadata->isReadOnly()); 31 | $this->assertSame('fieldName', $metadata->getFieldName()); 32 | $this->assertSame('targetPropertyName', $metadata->getTargetPropertyName()); 33 | $this->assertSame('targetFieldName', $metadata->getTargetFieldName()); 34 | $this->assertFalse($metadata->hasEagerHydration()); 35 | } 36 | 37 | public function testSettingEagerHydration() 38 | { 39 | $metadata = new OneToOne('', '', '', '', '', true, true, '', '', true); 40 | $this->assertTrue($metadata->hasEagerHydration()); 41 | } 42 | 43 | public function testExceptionOnMissingProperties() 44 | { 45 | $this->expectException(InvalidArgumentException::class); 46 | new OneToOne( 47 | 'propertyName', 48 | 'targetTable', 49 | 'targetEntity', 50 | 'targetFieldName', 51 | 'targetInterfaceName', 52 | true, 53 | false 54 | ); 55 | } 56 | 57 | public function testOptionalPropertiesAreSetToNullOnInverseSide() 58 | { 59 | $metadata = new OneToOne( 60 | 'propertyName', 61 | 'targetTable', 62 | 'targetEntity', 63 | 'targetFieldName', 64 | 'targetInterfaceName', 65 | false, 66 | false, 67 | 'foo', 68 | 'bar' 69 | ); 70 | 71 | $this->assertTrue($metadata->isReadOnly()); 72 | 73 | $this->expectException(InvalidArgumentException::class); 74 | $metadata->getTargetPropertyName(); 75 | 76 | $this->expectException(InvalidArgumentException::class); 77 | $metadata->getPropertyName(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/RecordIdTest.php: -------------------------------------------------------------------------------- 1 | assertSame('propertyName', $metadata->getPropertyName()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/BuiltInTypes.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/CustomType.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/EagerHydration.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 16 | 23 | 30 | 31 | 39 | 48 | 57 | 58 | 66 | 75 | 84 | 85 | 91 | 98 | 105 | 106 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/Embeddable.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/EmbeddedEntity.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/Empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/InterfaceName.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/Invalid.Xml.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/ManyToOne.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/ManyToOneWithoutInterface.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/Non.Xml.xml: -------------------------------------------------------------------------------- 1 | non-xml 2 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/OneToMany.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/OneToOneInverse.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/OneToOneOwning.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/ReadOnly.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 22 | 31 | 40 | 41 | 49 | 58 | 67 | 68 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/RecordId.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/RelationTarget.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/RelationTargetWithoutInterface.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /test/Repository/Builder/Metadata/TestAssets/Repeatable.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/Repository/Builder/Proxy/TestAssets/ComplexSetterInterface.php: -------------------------------------------------------------------------------- 1 | prophesize(MetadataBuilderInterface::class); 20 | $metadataBuilder->getMetadata('foo')->willReturn(new Entity('bar', 'foo', [], [], [], [], [])); 21 | 22 | $builder = new RepositoryBuilder( 23 | $this->prophesize(ResultSetClientInterface::class)->reveal(), 24 | $metadataBuilder->reveal(), 25 | $this->prophesize(ProxyBuilderInterface::class)->reveal() 26 | ); 27 | 28 | $this->assertSame($builder->buildRepository('foo'), $builder->buildRepository('foo')); 29 | } 30 | 31 | public function testMetadataInjection() 32 | { 33 | $metadata = new Entity('bar', 'foo', [], [], [], [], []); 34 | 35 | $metadataBuilder = $this->prophesize(MetadataBuilderInterface::class); 36 | $metadataBuilder->getMetadata('foo')->willReturn($metadata); 37 | 38 | $builder = new RepositoryBuilder( 39 | $this->prophesize(ResultSetClientInterface::class)->reveal(), 40 | $metadataBuilder->reveal(), 41 | $this->prophesize(ProxyBuilderInterface::class)->reveal() 42 | ); 43 | 44 | $repository = $builder->buildRepository('foo'); 45 | $this->assertAttributeSame('bar', 'layout', $repository); 46 | 47 | $hydration = self::getObjectAttribute($repository, 'hydration'); 48 | $this->assertInstanceOf(MetadataHydration::class, $hydration); 49 | $this->assertAttributeSame($builder, 'repositoryBuilder', $hydration); 50 | $this->assertAttributeSame($metadata, 'entityMetadata', $hydration); 51 | 52 | $extraction = self::getObjectAttribute($repository, 'extraction'); 53 | $this->assertInstanceOf(MetadataExtraction::class, $extraction); 54 | $this->assertAttributeSame($metadata, 'entityMetadata', $extraction); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/Repository/Builder/TestAssets/EmptyEntityInterface.php: -------------------------------------------------------------------------------- 1 | assertSame($expectedResult, $type->fromFileMakerValue($fileMakerValue)); 36 | } 37 | 38 | public function testUnsuccessfulConversionFromFileMaker() 39 | { 40 | $type = new BooleanType(); 41 | $this->assertTrue($type->fromFileMakerValue(new stdClass())); 42 | } 43 | 44 | public function testSuccessfulConversionToFileMaker() 45 | { 46 | $type = new BooleanType(); 47 | $this->assertSame(0, $type->toFileMakerValue(false)->asInteger()); 48 | $this->assertSame(1, $type->toFileMakerValue(true)->asInteger()); 49 | } 50 | 51 | public function testUnsuccessfulConversionToFileMaker() 52 | { 53 | $type = new BooleanType(); 54 | $this->expectException(InvalidArgumentException::class); 55 | $type->toFileMakerValue('foo'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/DateTimeTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame($value, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new DateTimeType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new DateTimeType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $type = new DateTimeType(); 35 | $value = new DateTimeImmutable(); 36 | $this->assertSame($value, $type->toFileMakerValue($value)); 37 | } 38 | 39 | public function testNullConversionToFileMaker() 40 | { 41 | $this->assertNull((new DateTimeType())->toFileMakerValue(null)); 42 | } 43 | 44 | public function testUnsuccessfulConversionToFileMaker() 45 | { 46 | $type = new DateTimeType(); 47 | $this->expectException(InvalidArgumentException::class); 48 | $type->toFileMakerValue('foo'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/DateTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame($value, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new DateType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new DateType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $this->assertSame( 35 | '01/23/4567', 36 | (new DateType())->toFileMakerValue(DateTimeImmutable::createFromFormat('!m/d/Y', '01/23/4567')) 37 | ); 38 | } 39 | 40 | public function testNullConversionToFileMaker() 41 | { 42 | $this->assertNull((new DateType())->toFileMakerValue(null)); 43 | } 44 | 45 | public function testUnsuccessfulConversionToFileMaker() 46 | { 47 | $type = new DateType(); 48 | $this->expectException(InvalidArgumentException::class); 49 | $type->toFileMakerValue('foo'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/DecimalTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame($value, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new DecimalType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new DecimalType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $type = new DecimalType(); 35 | $value = Decimal::fromInteger(1); 36 | $this->assertSame($value, $type->toFileMakerValue($value)); 37 | } 38 | 39 | public function testNullConversionToFileMaker() 40 | { 41 | $this->assertNull((new DecimalType())->toFileMakerValue(null)); 42 | } 43 | 44 | public function testUnsuccessfulConversionToFileMaker() 45 | { 46 | $type = new DecimalType(); 47 | $this->expectException(InvalidArgumentException::class); 48 | $type->toFileMakerValue('foo'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/FloatTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1.1, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new FloatType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new FloatType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $type = new FloatType(); 35 | $this->assertSame(1.1, $type->toFileMakerValue(1.1)->asFloat()); 36 | } 37 | 38 | public function testNullConversionToFileMaker() 39 | { 40 | $this->assertNull((new FloatType())->toFileMakerValue(null)); 41 | } 42 | 43 | public function testUnsuccessfulConversionToFileMaker() 44 | { 45 | $type = new FloatType(); 46 | $this->expectException(InvalidArgumentException::class); 47 | $type->toFileMakerValue('foo'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/IntegerTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new IntegerType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new IntegerType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $type = new IntegerType(); 35 | $this->assertSame(1, $type->toFileMakerValue(1)->asInteger()); 36 | } 37 | 38 | public function testNullConversionToFileMaker() 39 | { 40 | $this->assertNull((new IntegerType())->toFileMakerValue(null)); 41 | } 42 | 43 | public function testUnsuccessfulConversionToFileMaker() 44 | { 45 | $type = new IntegerType(); 46 | $this->expectException(InvalidArgumentException::class); 47 | $type->toFileMakerValue('foo'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/NullableStringTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', $type->fromFileMakerValue('foo')); 16 | } 17 | 18 | public function testNullConversionFromFileMaker() 19 | { 20 | $type = new NullableStringType(); 21 | $this->assertNull($type->fromFileMakerValue('')); 22 | } 23 | 24 | public function testUnsuccessfulConversionFromFileMaker() 25 | { 26 | $type = new NullableStringType(); 27 | $this->expectException(InvalidArgumentException::class); 28 | $type->fromFileMakerValue(1); 29 | } 30 | 31 | public function testSuccessfulConversionToFileMaker() 32 | { 33 | $type = new NullableStringType(); 34 | $this->assertSame('foo', $type->toFileMakerValue('foo')); 35 | } 36 | 37 | public function testNullConversionToFileMaker() 38 | { 39 | $type = new NullableStringType(); 40 | $this->assertSame('', $type->toFileMakerValue(null)); 41 | } 42 | 43 | public function testUnsuccessfulConversionToFileMaker() 44 | { 45 | $type = new NullableStringType(); 46 | $this->expectException(InvalidArgumentException::class); 47 | $type->toFileMakerValue(1); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/StreamTypeTest.php: -------------------------------------------------------------------------------- 1 | prophesize(StreamInterface::class)->reveal(); 18 | $this->assertSame($value, $type->fromFileMakerValue($value)); 19 | } 20 | 21 | public function testNullConversionFromFileMaker() 22 | { 23 | $this->assertNull((new StreamType())->fromFileMakerValue(null)); 24 | } 25 | 26 | public function testUnsuccessfulConversionFromFileMaker() 27 | { 28 | $type = new StreamType(); 29 | $this->expectException(InvalidArgumentException::class); 30 | $type->fromFileMakerValue('foo'); 31 | } 32 | 33 | public function testExceptionOnConversionToFileMaker() 34 | { 35 | $this->expectException(DomainException::class); 36 | $this->expectExceptionMessage('Attempted conversion to FileMaker value'); 37 | (new StreamType())->toFileMakerValue($this->prophesize(StreamInterface::class)->reveal()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/StringTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', $type->fromFileMakerValue('foo')); 16 | } 17 | 18 | public function testEmptyStringConversionFromFileMaker() 19 | { 20 | $type = new StringType(); 21 | $this->assertSame('', $type->fromFileMakerValue('')); 22 | } 23 | 24 | public function testUnsuccessfulConversionFromFileMaker() 25 | { 26 | $type = new StringType(); 27 | $this->expectException(InvalidArgumentException::class); 28 | $type->fromFileMakerValue(1); 29 | } 30 | 31 | public function testSuccessfulConversionToFileMaker() 32 | { 33 | $type = new StringType(); 34 | $this->assertSame('foo', $type->toFileMakerValue('foo')); 35 | } 36 | 37 | public function testUnsuccessfulConversionToFileMaker() 38 | { 39 | $type = new StringType(); 40 | $this->expectException(InvalidArgumentException::class); 41 | $type->toFileMakerValue(1); 42 | } 43 | 44 | public function testUnsuccessfulNullConversionToFileMaker() 45 | { 46 | $type = new StringType(); 47 | $this->expectException(InvalidArgumentException::class); 48 | $type->toFileMakerValue(null); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Repository/Builder/Type/TimeTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame($value, $type->fromFileMakerValue($value)); 18 | } 19 | 20 | public function testNullConversionFromFileMaker() 21 | { 22 | $this->assertNull((new TimeType())->fromFileMakerValue(null)); 23 | } 24 | 25 | public function testUnsuccessfulConversionFromFileMaker() 26 | { 27 | $type = new TimeType(); 28 | $this->expectException(InvalidArgumentException::class); 29 | $type->fromFileMakerValue('foo'); 30 | } 31 | 32 | public function testSuccessfulConversionToFileMaker() 33 | { 34 | $this->assertSame( 35 | '01:23:45', 36 | (new TimeType())->toFileMakerValue(DateTimeImmutable::createFromFormat('!H:i:s', '01:23:45')) 37 | ); 38 | } 39 | 40 | public function testNullConversionToFileMaker() 41 | { 42 | $this->assertNull((new TimeType())->toFileMakerValue(null)); 43 | } 44 | 45 | public function testUnsuccessfulConversionToFileMaker() 46 | { 47 | $type = new TimeType(); 48 | $this->expectException(InvalidArgumentException::class); 49 | $type->toFileMakerValue('foo'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/Repository/LazyLoadedCollectionTest.php: -------------------------------------------------------------------------------- 1 | prophesize(RepositoryInterface::class); 23 | $repository->findByQuery(Argument::any())->will(function (array $parameters) use ( 24 | $testCase, 25 | $first, 26 | $second, 27 | $third 28 | ) { 29 | $testCase->assertSame([ 30 | '-query' => '(q1);(q2);(q3)', 31 | '-q1' => 'foo', 32 | '-q1.value' => '1', 33 | '-q2' => 'foo', 34 | '-q2.value' => '2', 35 | '-q3' => 'foo', 36 | '-q3.value' => '3', 37 | ], $parameters[0]->toParameters()); 38 | 39 | return new ItemCollection([ 40 | $first, 41 | $second, 42 | $third, 43 | ], 3); 44 | }); 45 | 46 | $collection = new LazyLoadedCollection($repository->reveal(), 'foo', [ 47 | ['foo' => 1], 48 | ['foo' => 2], 49 | ['foo' => 3], 50 | ]); 51 | $entities = []; 52 | 53 | $this->assertFalse($collection->isEmpty()); 54 | 55 | foreach ($collection as $entity) { 56 | $entities[] = $entity; 57 | } 58 | 59 | $this->assertSame($first, $entities[0]); 60 | $this->assertSame($second, $entities[1]); 61 | $this->assertSame($third, $entities[2]); 62 | } 63 | 64 | public function testEmptyCollection() 65 | { 66 | $collection = new LazyLoadedCollection($this->prophesize(RepositoryInterface::class)->reveal(), 'foo', []); 67 | $this->assertTrue($collection->isEmpty()); 68 | $this->assertNull($collection->first()); 69 | } 70 | 71 | public function testIteratorCaching() 72 | { 73 | $collection = new LazyLoadedCollection($this->prophesize(RepositoryInterface::class)->reveal(), 'foo', []); 74 | $this->assertSame($collection->getIterator(), $collection->getIterator()); 75 | } 76 | 77 | public function testFirst() 78 | { 79 | $first = new stdClass(); 80 | $repository = $this->prophesize(RepositoryInterface::class); 81 | $repository->findByQuery(Argument::any())->will(function () use ($first) { 82 | return new ItemCollection([ 83 | $first, 84 | new stdClass(), 85 | new stdClass(), 86 | ], 3); 87 | }); 88 | 89 | $collection = new LazyLoadedCollection($repository->reveal(), 'foo', [ 90 | ['foo' => 1], 91 | ['foo' => 2], 92 | ['foo' => 3], 93 | ]); 94 | $this->assertSame($first, $collection->first()); 95 | } 96 | 97 | public function testCount() 98 | { 99 | $collection = new LazyLoadedCollection( 100 | $this->prophesize(RepositoryInterface::class)->reveal(), 101 | 'foo', 102 | [ 103 | ['foo' => 1], 104 | ['foo' => 2], 105 | ['foo' => 3], 106 | ] 107 | ); 108 | $this->assertSame(3, count($collection)); 109 | } 110 | 111 | public function testGetTotalCount() 112 | { 113 | $collection = new LazyLoadedCollection( 114 | $this->prophesize(RepositoryInterface::class)->reveal(), 115 | 'foo', 116 | [] 117 | ); 118 | $this->assertSame(0, $collection->getTotalCount()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/Repository/Query/FindQueryTest.php: -------------------------------------------------------------------------------- 1 | expectException(EmptyQueryException::class); 19 | $findQuery->toParameters(); 20 | } 21 | 22 | public function testAddNoOrQueries() 23 | { 24 | $findQuery = new FindQuery(); 25 | 26 | $this->expectException(InvalidArgumentException::class); 27 | $this->expectExceptionMessage('cannot be empty'); 28 | $findQuery->addOrQueries(); 29 | } 30 | 31 | public function testAddNoAndQueries() 32 | { 33 | $findQuery = new FindQuery(); 34 | 35 | $this->expectException(InvalidArgumentException::class); 36 | $this->expectExceptionMessage('cannot be empty'); 37 | $findQuery->addAndQueries(); 38 | } 39 | 40 | public function testAddMultipleAndQueries() 41 | { 42 | $findQuery = new FindQuery(); 43 | $findQuery->addAndQueries(new Query('foo', 'bar'), new Query('baz', 'bat', true)); 44 | $this->assertSame([ 45 | '-query' => '(q1,!q2)', 46 | '-q1' => 'foo', 47 | '-q1.value' => 'bar', 48 | '-q2' => 'baz', 49 | '-q2.value' => 'bat', 50 | ], $findQuery->toParameters()); 51 | } 52 | 53 | public function testAddMultipleOrQueries() 54 | { 55 | $findQuery = new FindQuery(); 56 | $findQuery->addOrQueries(new Query('foo', 'bar'), new Query('baz', 'bat', true)); 57 | $this->assertSame([ 58 | '-query' => '(q1);(!q2)', 59 | '-q1' => 'foo', 60 | '-q1.value' => 'bar', 61 | '-q2' => 'baz', 62 | '-q2.value' => 'bat', 63 | ], $findQuery->toParameters()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/Repository/Query/QueryTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', $query->getFieldName()); 15 | $this->assertSame('bar', $query->getValue()); 16 | $this->assertFalse($query->isExclude()); 17 | } 18 | 19 | public function testWithExcludeParameter() 20 | { 21 | $query = new Query('foo', 'bar', true); 22 | $this->assertTrue($query->isExclude()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /xsd/entity-metadata-5-0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | --------------------------------------------------------------------------------