├── .github ├── FUNDING.yml └── workflows │ ├── php-src-8.1.dockerfile │ ├── php-src-8.2.dockerfile │ ├── php-src-8.3.dockerfile │ ├── php-src-8.4.dockerfile │ ├── php-src-master.dockerfile │ ├── php-src.yml │ ├── php.yml │ └── phpstan.yml ├── .gitignore ├── README.md ├── Sample_results.ipynb ├── composer.json ├── composer.lock ├── docker-compose.yml ├── samples ├── FMDataAPI_Sample.php ├── HISTORY.md └── cat.jpg ├── src ├── FMDataAPI.php └── Supporting │ ├── CommunicationProvider.php │ ├── FileMakerLayout.php │ └── FileMakerRelation.php └── test ├── .phpunit.result.cache ├── FMDataAPIUnitTest.php ├── HashForTestInput.php ├── TestProvider.php ├── phpstan-baseline.neon ├── phpstan.neon └── phpunit.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: msyk 4 | -------------------------------------------------------------------------------- /.github/workflows/php-src-8.1.dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:22.04 2 | RUN export DEBIAN_FRONTEND=noninteractive \ 3 | && apt update && apt install -y --no-install-recommends \ 4 | software-properties-common \ 5 | ca-certificates \ 6 | wget \ 7 | tar \ 8 | git \ 9 | pkg-config build-essential \ 10 | libssl-dev \ 11 | autoconf \ 12 | gcc \ 13 | make \ 14 | curl \ 15 | unzip \ 16 | bison \ 17 | re2c \ 18 | locales \ 19 | ldap-utils \ 20 | openssl \ 21 | slapd \ 22 | language-pack-de \ 23 | libgmp-dev \ 24 | libicu-dev \ 25 | libtidy-dev \ 26 | libenchant-2-dev \ 27 | libbz2-dev \ 28 | libsasl2-dev \ 29 | libxpm-dev \ 30 | libzip-dev \ 31 | libsqlite3-dev \ 32 | libwebp-dev \ 33 | libonig-dev \ 34 | libkrb5-dev \ 35 | libgssapi-krb5-2 \ 36 | libcurl4-openssl-dev \ 37 | libxml2-dev \ 38 | libxslt1-dev \ 39 | libpq-dev \ 40 | libreadline-dev \ 41 | libldap2-dev \ 42 | libsodium-dev \ 43 | libargon2-dev \ 44 | libmm-dev \ 45 | libsnmp-dev \ 46 | postgresql \ 47 | postgresql-contrib \ 48 | snmpd \ 49 | snmp-mibs-downloader \ 50 | freetds-dev \ 51 | unixodbc-dev \ 52 | llvm \ 53 | clang \ 54 | dovecot-core \ 55 | dovecot-pop3d \ 56 | dovecot-imapd \ 57 | sendmail \ 58 | firebird-dev \ 59 | liblmdb-dev \ 60 | libtokyocabinet-dev \ 61 | libdb-dev \ 62 | libqdbm-dev \ 63 | libjpeg-dev \ 64 | libpng-dev \ 65 | libfreetype6-dev \ 66 | && apt -y clean \ 67 | && rm -rf /var/lib/apt/lists/* 68 | RUN git clone --depth 1 --branch PHP-8.1 https://github.com/php/php-src.git 69 | RUN cd php-src; export CC=clang; export CXX=clang++; export CFLAGS="-DZEND_TRACK_ARENA_ALLOC"; ./buildconf --force; ./configure --enable-debug --enable-mbstring --with-openssl --with-curl; make -j$(/usr/bin/nproc); make TEST_PHP_ARGS=-j$(/usr/bin/nproc) test; make install 70 | COPY composer.json /composer.json 71 | COPY composer.lock /composer.lock 72 | COPY src /src 73 | COPY test /test 74 | RUN curl -sS https://getcomposer.org/installer | php; mv composer.phar /usr/local/bin/composer; chmod +x /usr/local/bin/composer 75 | RUN cd / && composer update 76 | #RUN composer test 77 | CMD [ "/sbin/init" ] 78 | -------------------------------------------------------------------------------- /.github/workflows/php-src-8.2.dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:22.04 2 | RUN export DEBIAN_FRONTEND=noninteractive \ 3 | && apt update && apt install -y --no-install-recommends \ 4 | software-properties-common \ 5 | ca-certificates \ 6 | wget \ 7 | tar \ 8 | git \ 9 | pkg-config build-essential \ 10 | libssl-dev \ 11 | autoconf \ 12 | gcc \ 13 | make \ 14 | curl \ 15 | unzip \ 16 | bison \ 17 | re2c \ 18 | locales \ 19 | ldap-utils \ 20 | openssl \ 21 | slapd \ 22 | language-pack-de \ 23 | libgmp-dev \ 24 | libicu-dev \ 25 | libtidy-dev \ 26 | libenchant-2-dev \ 27 | libbz2-dev \ 28 | libsasl2-dev \ 29 | libxpm-dev \ 30 | libzip-dev \ 31 | libsqlite3-dev \ 32 | libwebp-dev \ 33 | libonig-dev \ 34 | libkrb5-dev \ 35 | libgssapi-krb5-2 \ 36 | libcurl4-openssl-dev \ 37 | libxml2-dev \ 38 | libxslt1-dev \ 39 | libpq-dev \ 40 | libreadline-dev \ 41 | libldap2-dev \ 42 | libsodium-dev \ 43 | libargon2-dev \ 44 | libmm-dev \ 45 | libsnmp-dev \ 46 | postgresql \ 47 | postgresql-contrib \ 48 | snmpd \ 49 | snmp-mibs-downloader \ 50 | freetds-dev \ 51 | unixodbc-dev \ 52 | llvm \ 53 | clang \ 54 | dovecot-core \ 55 | dovecot-pop3d \ 56 | dovecot-imapd \ 57 | sendmail \ 58 | firebird-dev \ 59 | liblmdb-dev \ 60 | libtokyocabinet-dev \ 61 | libdb-dev \ 62 | libqdbm-dev \ 63 | libjpeg-dev \ 64 | libpng-dev \ 65 | libfreetype6-dev \ 66 | && apt -y clean \ 67 | && rm -rf /var/lib/apt/lists/* 68 | RUN git clone --depth 1 --branch PHP-8.2 https://github.com/php/php-src.git 69 | RUN cd php-src; export CC=clang; export CXX=clang++; export CFLAGS="-DZEND_TRACK_ARENA_ALLOC"; ./buildconf --force; ./configure --enable-debug --enable-mbstring --with-openssl --with-curl; make -j$(/usr/bin/nproc); make TEST_PHP_ARGS=-j$(/usr/bin/nproc) test; make install 70 | COPY composer.json /composer.json 71 | COPY composer.lock /composer.lock 72 | COPY src /src 73 | COPY test /test 74 | RUN curl -sS https://getcomposer.org/installer | php; mv composer.phar /usr/local/bin/composer; chmod +x /usr/local/bin/composer 75 | RUN cd / && composer update 76 | #RUN composer test 77 | CMD [ "/sbin/init" ] 78 | -------------------------------------------------------------------------------- /.github/workflows/php-src-8.3.dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:22.04 2 | RUN export DEBIAN_FRONTEND=noninteractive \ 3 | && apt update && apt install -y --no-install-recommends \ 4 | software-properties-common \ 5 | ca-certificates \ 6 | wget \ 7 | tar \ 8 | git \ 9 | pkg-config build-essential \ 10 | libssl-dev \ 11 | autoconf \ 12 | gcc \ 13 | make \ 14 | curl \ 15 | unzip \ 16 | bison \ 17 | re2c \ 18 | locales \ 19 | ldap-utils \ 20 | openssl \ 21 | slapd \ 22 | language-pack-de \ 23 | libgmp-dev \ 24 | libicu-dev \ 25 | libtidy-dev \ 26 | libenchant-2-dev \ 27 | libbz2-dev \ 28 | libsasl2-dev \ 29 | libxpm-dev \ 30 | libzip-dev \ 31 | libsqlite3-dev \ 32 | libwebp-dev \ 33 | libonig-dev \ 34 | libkrb5-dev \ 35 | libgssapi-krb5-2 \ 36 | libcurl4-openssl-dev \ 37 | libxml2-dev \ 38 | libxslt1-dev \ 39 | libpq-dev \ 40 | libreadline-dev \ 41 | libldap2-dev \ 42 | libsodium-dev \ 43 | libargon2-dev \ 44 | libmm-dev \ 45 | libsnmp-dev \ 46 | postgresql \ 47 | postgresql-contrib \ 48 | snmpd \ 49 | snmp-mibs-downloader \ 50 | freetds-dev \ 51 | unixodbc-dev \ 52 | llvm \ 53 | clang \ 54 | dovecot-core \ 55 | dovecot-pop3d \ 56 | dovecot-imapd \ 57 | sendmail \ 58 | firebird-dev \ 59 | liblmdb-dev \ 60 | libtokyocabinet-dev \ 61 | libdb-dev \ 62 | libqdbm-dev \ 63 | libjpeg-dev \ 64 | libpng-dev \ 65 | libfreetype6-dev \ 66 | && apt -y clean \ 67 | && rm -rf /var/lib/apt/lists/* 68 | RUN git clone --depth 1 --branch PHP-8.3 https://github.com/php/php-src.git 69 | RUN cd php-src; export CC=clang; export CXX=clang++; export CFLAGS="-DZEND_TRACK_ARENA_ALLOC"; ./buildconf --force; ./configure --enable-debug --enable-mbstring --with-openssl --with-curl; make -j$(/usr/bin/nproc); make TEST_PHP_ARGS=-j$(/usr/bin/nproc) test; make install 70 | COPY composer.json /composer.json 71 | COPY composer.lock /composer.lock 72 | COPY src /src 73 | COPY test /test 74 | RUN curl -sS https://getcomposer.org/installer | php; mv composer.phar /usr/local/bin/composer; chmod +x /usr/local/bin/composer 75 | RUN cd / && composer update 76 | #RUN composer test 77 | CMD [ "/sbin/init" ] 78 | -------------------------------------------------------------------------------- /.github/workflows/php-src-8.4.dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:22.04 2 | RUN export DEBIAN_FRONTEND=noninteractive \ 3 | && apt update && apt install -y --no-install-recommends \ 4 | software-properties-common \ 5 | ca-certificates \ 6 | wget \ 7 | tar \ 8 | git \ 9 | pkg-config build-essential \ 10 | libssl-dev \ 11 | autoconf \ 12 | gcc \ 13 | make \ 14 | curl \ 15 | unzip \ 16 | bison \ 17 | re2c \ 18 | locales \ 19 | ldap-utils \ 20 | openssl \ 21 | slapd \ 22 | language-pack-de \ 23 | libgmp-dev \ 24 | libicu-dev \ 25 | libtidy-dev \ 26 | libenchant-2-dev \ 27 | libbz2-dev \ 28 | libsasl2-dev \ 29 | libxpm-dev \ 30 | libzip-dev \ 31 | libsqlite3-dev \ 32 | libsqlite3-mod-spatialite \ 33 | libwebp-dev \ 34 | libonig-dev \ 35 | libcurl4-openssl-dev \ 36 | libxml2-dev \ 37 | libxslt1-dev \ 38 | libpq-dev \ 39 | libreadline-dev \ 40 | libldap2-dev \ 41 | libsodium-dev \ 42 | libargon2-0-dev \ 43 | libmm-dev \ 44 | libsnmp-dev \ 45 | postgresql \ 46 | postgresql-contrib \ 47 | snmpd \ 48 | snmp-mibs-downloader \ 49 | freetds-dev \ 50 | unixodbc-dev \ 51 | llvm \ 52 | clang \ 53 | dovecot-core \ 54 | dovecot-pop3d \ 55 | dovecot-imapd \ 56 | sendmail \ 57 | firebird-dev \ 58 | liblmdb-dev \ 59 | libtokyocabinet-dev \ 60 | libdb-dev \ 61 | libqdbm-dev \ 62 | libjpeg-dev \ 63 | libpng-dev \ 64 | libfreetype6-dev \ 65 | && apt -y clean \ 66 | && rm -rf /var/lib/apt/lists/* 67 | RUN git clone --depth 1 --branch PHP-8.4 https://github.com/php/php-src.git 68 | RUN cd php-src; export CC=clang; export CXX=clang++; export CFLAGS="-DZEND_TRACK_ARENA_ALLOC"; ./buildconf --force; ./configure --enable-debug --enable-mbstring --with-openssl --with-curl; make -j$(/usr/bin/nproc); make TEST_PHP_ARGS=-j$(/usr/bin/nproc) test; make install 69 | COPY composer.json /composer.json 70 | COPY composer.lock /composer.lock 71 | COPY src /src 72 | COPY test /test 73 | RUN curl -sS https://getcomposer.org/installer | php; mv composer.phar /usr/local/bin/composer; chmod +x /usr/local/bin/composer 74 | RUN cd / && composer update 75 | #RUN composer test 76 | CMD [ "/sbin/init" ] 77 | -------------------------------------------------------------------------------- /.github/workflows/php-src-master.dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:22.04 2 | RUN export DEBIAN_FRONTEND=noninteractive \ 3 | && apt update && apt install -y --no-install-recommends \ 4 | software-properties-common \ 5 | ca-certificates \ 6 | wget \ 7 | tar \ 8 | git \ 9 | pkg-config build-essential \ 10 | libssl-dev \ 11 | autoconf \ 12 | gcc \ 13 | make \ 14 | curl \ 15 | unzip \ 16 | bison \ 17 | re2c \ 18 | locales \ 19 | ldap-utils \ 20 | openssl \ 21 | slapd \ 22 | language-pack-de \ 23 | libgmp-dev \ 24 | libicu-dev \ 25 | libtidy-dev \ 26 | libenchant-2-dev \ 27 | libbz2-dev \ 28 | libsasl2-dev \ 29 | libxpm-dev \ 30 | libzip-dev \ 31 | libsqlite3-dev \ 32 | libsqlite3-mod-spatialite \ 33 | libwebp-dev \ 34 | libonig-dev \ 35 | libcurl4-openssl-dev \ 36 | libxml2-dev \ 37 | libxslt1-dev \ 38 | libpq-dev \ 39 | libreadline-dev \ 40 | libldap2-dev \ 41 | libsodium-dev \ 42 | libargon2-0-dev \ 43 | libmm-dev \ 44 | libsnmp-dev \ 45 | postgresql \ 46 | postgresql-contrib \ 47 | snmpd \ 48 | snmp-mibs-downloader \ 49 | freetds-dev \ 50 | unixodbc-dev \ 51 | llvm \ 52 | clang \ 53 | dovecot-core \ 54 | dovecot-pop3d \ 55 | dovecot-imapd \ 56 | sendmail \ 57 | firebird-dev \ 58 | liblmdb-dev \ 59 | libtokyocabinet-dev \ 60 | libdb-dev \ 61 | libqdbm-dev \ 62 | libjpeg-dev \ 63 | libpng-dev \ 64 | libfreetype6-dev \ 65 | && apt -y clean \ 66 | && rm -rf /var/lib/apt/lists/* 67 | RUN git clone --depth 1 --branch master https://github.com/php/php-src.git 68 | RUN cd php-src; export CC=clang; export CXX=clang++; export CFLAGS="-DZEND_TRACK_ARENA_ALLOC"; ./buildconf --force; ./configure --enable-debug --enable-mbstring --with-openssl --with-curl; make -j$(/usr/bin/nproc); make TEST_PHP_ARGS=-j$(/usr/bin/nproc) test; make install 69 | COPY composer.json /composer.json 70 | COPY composer.lock /composer.lock 71 | COPY src /src 72 | COPY test /test 73 | RUN curl -sS https://getcomposer.org/installer | php; mv composer.phar /usr/local/bin/composer; chmod +x /usr/local/bin/composer 74 | RUN cd / && composer update 75 | #RUN composer test 76 | CMD [ "/sbin/init" ] 77 | -------------------------------------------------------------------------------- /.github/workflows/php-src.yml: -------------------------------------------------------------------------------- 1 | name: Test with php-src 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: '0 9 7,14,21,28 * *' 9 | 10 | jobs: 11 | test: 12 | name: Test with php-src 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | php: [ '8.1', '8.2', '8.3', '8.4', 'master' ] 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - name: Run docker compose 20 | shell: bash 21 | run: | 22 | cp .github/workflows/php-src-${{ matrix.php }}.dockerfile Dockerfile 23 | docker compose up -d 24 | sleep 30 25 | 26 | - name: Run testing 27 | shell: bash 28 | run: | 29 | sleep 30 30 | docker compose run web sh -c "cd / && composer test" 31 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: [ 'master', 'main' ] 9 | workflow_dispatch: 10 | schedule: 11 | - cron: '0 9 15 * *' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ${{ matrix.os }} 17 | env: 18 | PHP_EXTENSIONS: mbstring, json, bcmath, zip, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, exif, gd, ldap, fileinfo 19 | strategy: 20 | matrix: 21 | # https://github.com/shivammathur/setup-php?tab=readme-ov-file#cloud-osplatform-support 22 | os: [ 'ubuntu-22.04', 'windows-2022', 'macos-14' ] 23 | php-version: [ '8.1', '8.2', '8.3', '8.4' ] 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | 28 | - name: Install PHP with extensions 29 | uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0 30 | with: 31 | php-version: ${{ matrix.php-version }} 32 | coverage: none 33 | extensions: ${{ env.PHP_EXTENSIONS }} 34 | 35 | - name: Prepare environment 36 | run: composer update 37 | 38 | - name: Run testing 39 | run: | 40 | php -v 41 | composer test 42 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: [ 'master', 'main' ] 9 | pull_request_target: 10 | types: 11 | - closed 12 | 13 | jobs: 14 | run: 15 | name: Run PHPStan 16 | runs-on: 'ubuntu-latest' 17 | strategy: 18 | matrix: 19 | level: [ 1, 2 ] 20 | include: 21 | - current-level: 1 22 | - max-level: 2 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0 28 | with: 29 | php-version: '8.4' 30 | 31 | - name: Remove phpDocumentor temporarily and Install PHPStan 32 | run: composer remove --dev --no-update phpdocumentor/phpdocumentor; composer update 33 | 34 | - name: Restore cached baseline for PHPStan 35 | id: cache-baseline-restore 36 | uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 37 | with: 38 | path: | 39 | test/phpstan-baseline.neon 40 | key: phpstan-baseline-${{ github.run_id }}" 41 | restore-keys: | 42 | phpstan-baseline- 43 | 44 | - name: Run PHPStan 45 | if: matrix.level == matrix.current-level 46 | continue-on-error: true 47 | run: | 48 | ./vendor/bin/phpstan analyse --memory-limit 1G -c test/phpstan.neon src test -l "${{ matrix.level }}" 49 | 50 | - name: Run PHPStan 51 | if: matrix.level > matrix.current-level 52 | continue-on-error: true 53 | run: | 54 | ./vendor/bin/phpstan analyse --memory-limit 1G -c test/phpstan.neon src test -l "${{ matrix.level }}" 55 | exit 0 56 | 57 | - name: Generate the baseline for PHPStan 58 | if: matrix.level == matrix.max-level && github.event.pull_request.merged == true 59 | continue-on-error: true 60 | run: | 61 | ./vendor/bin/phpstan analyse --memory-limit 1G -c test/phpstan.neon --generate-baseline test/phpstan-baseline.neon src test -vvv --debug -l "${{ matrix.level }}" 62 | exit 0 63 | 64 | - name: Save the baseline for PHPStan 65 | id: cache-baseline-save 66 | if: matrix.level == matrix.max-level && github.event.pull_request.merged == true 67 | uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 68 | with: 69 | path: | 70 | test/phpstan-baseline.neon 71 | key: phpstan-baseline-${{ github.run_id }}" 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor/ 3 | .phpdoc 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FMDataAPI Ver.33 [![Build Status](https://github.com/msyk/FMDataAPI/actions/workflows/php.yml/badge.svg)](https://github.com/msyk/FMDataAPI/actions/workflows/php.yml) 2 | 3 | by Masayuki Nii (nii@msyk.net) 4 | 5 | FMDataAPI is a class developed in PHP to access FileMaker database 6 | with Claris FileMaker Data API. 7 | 8 | ## Contributers 9 | 10 | They created pull requests. Thanks for cooperating. 11 | 12 | - Atsushi Matsuo 13 | - darnel 14 | - Craig Smith 15 | - Bernhard Schulz 16 | - montaniasystemab 17 | - Rickard Andersson 18 | - Julien @AnnoyingTechnology 19 | - Tom Kuijer 20 | - Thijs Meijer 21 | - Patrick Janser 22 | - Roger Engström 23 | - Stathis Askaridis 24 | 25 | ## At a Glance 26 | 27 | The FileMaker database named "TestDB.fmp12" is hosted on localhost, and 28 | it sets the "fmrest" as access privilege. The account to connect with REST API is "web" 29 | and "password." This database has the layout named "person_layout," and you 30 | can use the layout name as a property of the FMDataAPI instance. The return 31 | value of the "query" method is Iterator and can repeat in the foreach statement 32 | with each record in the query result. This layout has the field named 33 | "FamilyName" and "GivenName," and can use the field name as a property. 34 | 35 | ``` 36 | $fmdb = new FMDataAPI("TestDB", "web", "password"); 37 | $result = $fmdb->person_layout->query(); 38 | foreach ($result as $record) { 39 | echo "name: {$record->FamilyName}, {$record->GivenName}"; 40 | } 41 | ``` 42 | 43 | For more details, I'd like to read codes and comments in file samples/FMDataAPI_Sample.php. 44 | 45 | API Document is here: 46 | https://inter-mediator.info/FMDataAPI/packages/INTER-Mediator-FileMakerServer-RESTAPI.html 47 | ## What's This? 48 | 49 | The FileMaker Data API is the new feature of FileMaker Server 16, 50 | and it's the API with REST-based database operations. 51 | Although the Custom Web Publishing is the way to access the database 52 | for a long while, FileMaker Inc. has introduced the modern feature to operate 53 | the database. The current version of FMDataAPI works on just FileMaker 18 and 19 platform. 54 | 55 | For now, I'm focusing on developing the web application framework "INTER-Mediator" 56 | (https://inter-mediator.com/ or https://github.com/INTER-Mediator/INTER-Mediator.git) 57 | which can develop the core features of database-driven web application 58 | with declarative descriptions. INTER-Mediator has already supported the Custom 59 | Web Publishing with FX.php, and I develop codes here for support REST APIs. 60 | 61 | Bug reports and contributions are welcome. 62 | 63 | ## Installing to Your Project 64 | 65 | FMDataAPI has "composer.json," so you can add your composer.json file in your project as below. 66 | 67 | ``` 68 | ... 69 | "require": { 70 | ... 71 | "inter-mediator/fmdataapi":"33" 72 | } ... 73 | ``` 74 | 75 | ## About Files and Directories 76 | 77 | - src/FMDataAPI.php 78 | - The core class, and you just use this for your application. 79 | This class and supporting classes are object-oriented REST API 80 | wrappers. 81 | - src/Supporting/**.php 82 | - The supporting classes for the FMDataAPI class. Perhaps you don't need to create these classes, but you have to handle methods on them. 83 | - composer.json, composer.lock 84 | - Composer information files. 85 | - Sample_results.ipynb 86 | - Sample program and results with Jupyter Notebook style. Sorry for slight old version results. 87 | - samples/FMDataAPI_Sample.php and cat.jpg 88 | - This is the sample program of FMDataAPI class, and shows how to 89 | use FMDataAPI class. It includes rich comments, 90 | but Sample_results.ipynb is more informative. 91 | - README.md, .gitignore 92 | - These are for GitHub. 93 | - test 94 | - Some files for unit testing. 95 | - .github, docker-compose.yml 96 | - Files for GitHub Actions to run CI jobs. 97 | 98 | ## Licence 99 | 100 | MIT License 101 | 102 | ## Acknoledgement 103 | 104 | - Thanks to Atsushi Matsuo. Your script is quite helpful to implement the "localserver" feature. 105 | (https://gist.github.com/matsuo/ef5cb7c98bb494d507731886883bcbc1) Moreover, thanks for updating and fixing bugs. 106 | - Thanks to Frank Gonzalez. Your bug report is brilliant, and I could fix it quickly. 107 | - Thanks to base64bits for coding about container field. 108 | - Thanks to phpsa for bug fix. 109 | - Thanks to Flexboom for bug fix. 110 | - Thanks to schube for bug fix. 111 | - Thanks to frankeg for bug fix. 112 | 113 | ## History 114 | 115 | (Previous history is [here](samples/HISTORY.md)) 116 | 117 | - 2021-02-10: [Ver.22] 118 | Setting the timeout value about cURL. Thanks to @montaniasystemab. Also thanks to @AnnoyingTechnology for correcting. 119 | - 2021-11-11: [Ver.23] 120 | File structure is updated for PSR-4. Thanks to tkuijer. 121 | - 2021-12-23: [Ver.24] 122 | Bug fix for portal limit parameter. Thanks to tkuijer. 123 | - 2022-03-24: [Ver.25] 124 | Add methods(getFirstRecord, getLastRecord, getRecords) to the FileMakerRelation class. 125 | - 2022-03-26: [Ver.26] 126 | Add methods(setFieldHTMLEncoding, getFieldHTMLEncoding) to the FMDataAPI class. 127 | These are going to use for compatibility mode of FileMaker API for PHP. 128 | - 2022-06-06: [Ver.27] 129 | Dropped the support of PHP5, the minimum version is PHP 7.1, but 7.2 or later is recommended. 130 | - 2022-08-04: [Ver.28] 131 | Added the getContainerData(URL) method to the FMDataAPI class for accessing container data from the url containing /Streaming/MainDB. 132 | [BUG FIX] The FileMakerRelation class's toArray method didn't return an array (Thanks to Talwinder Singh). 133 | - 2022-12-28: [Ver.29] 134 | Fixed the 'HTTP/2 stream 0 was not closed cleanly' problem with the new FileMaker (Thanks to @thijsmeijer). 135 | Also fixed the getPortalNames issue for single record relation (Thanks to @PGMMattias). 136 | - 2023-06-20: [Ver.30] 137 | The toArray() method bug fixed. In same cases, it returned []. (Thanks to @PGMMattias). 138 | - 2023-11-24: [Ver.31] 139 | The curlErrorMessage() method returns the error message from curl (Thanks to @P1-Roger). 140 | Corrected phpdoc issue (Thanks to @patacra). 141 | - 2024-10-10: [Ver.32] 142 | From this version, the minimum PHP version is 8.1. 143 | Fix SSL certificate check errors by using the system's certificate authorities (Thanks to @patacra). 144 | FileMakerLayout::getMetadataOld and getMetadata methods don't return the false value in the case of log-in error. 145 | It returns just null. 146 | - 2025-03-19: [Ver.33] 147 | The query method supports date format parameter (Thanks to @stathisaska). 148 | The debug property of the CommunicationProvider class initializes the bool false value (Thanks to Bernhard). 149 | -------------------------------------------------------------------------------- /Sample_results.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# FMDataAPI Sample Results\n", 8 | "\n", 9 | "by Masayuki Nii (nii@msyk.net)\n", 10 | "\n", 11 | "FMDataAPI is a class developed by PHP to access FileMaker database with FileMaker Data API.\n", 12 | "\n", 13 | "The repository is https://github.com/msyk/FMDataAPI.\n", 14 | "\n", 15 | "API Document is http://inter-mediator.info/FMDataAPI/namespaces/INTERMediator.FileMakerServer.RESTAPI.html.\n", 16 | "\n", 17 | "The identifier of Composer is \"inter-mediator/fmdataapi\".\n", 18 | "\n", 19 | "Ths notebook aims to show the results of FMDataAPI in short hand. You don't have to prepare PHP even FileMaker Server because you can see all results with codes in this notebook." 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Preparing to Use FMDataAPI in PHP way\n", 27 | "\n", 28 | "First of all, the FMDataAPI.php file has to be included. All classes are defined in it. Of course, you can specify partial or full path, or composer based class path resolving." 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 1, 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "data": { 38 | "text/plain": [ 39 | "\u001b[35m1\u001b[39m" 40 | ] 41 | }, 42 | "execution_count": 1, 43 | "metadata": {}, 44 | "output_type": "execute_result" 45 | } 46 | ], 47 | "source": [ 48 | "include_once \"FMDataAPI.php\";" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "For your convenience, the main class name FMDataAPI is defined at the current namespace." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "use INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPI as FMDataAPI;" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "## Getting Access to FileMaker DB\n", 72 | "Instanticate the class FMDataAPI with database name, user name, password and host.Although the port number and protocol can be set in parameters of constractor, these parameters can be omitted with default values." 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "data": { 82 | "text/plain": [ 83 | "\u001b[34;4mINTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPI\u001b[39;24m {#200}" 84 | ] 85 | }, 86 | "execution_count": 3, 87 | "metadata": {}, 88 | "output_type": "execute_result" 89 | } 90 | ], 91 | "source": [ 92 | "$fmdb = new FMDataAPI(\"TestDB\", \"web\", \"password\", \"localserver\");" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "### Very Simple Query, Getting All Records\n", 100 | "The FMDataAPI has the property as the same name of layout. This sample database has the 'person_layout' layout, so '$fmdb->person_layout' refers FMLayout object fo the proxy of the layout. FMLayout class has the 'query' method and returns FileMakerRelation class's object. The condition spefied in parameter is same as FileMaker's Find Record API. This example means querying all record from the \"person_layout\" layout." 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 4, 106 | "metadata": {}, 107 | "outputs": [ 108 | { 109 | "data": { 110 | "text/plain": [ 111 | "\u001b[34;4mINTERMediator\\FileMakerServer\\RESTAPI\\Supporting\\FileMakerRelation\u001b[39;24m {#192}" 112 | ] 113 | }, 114 | "execution_count": 4, 115 | "metadata": {}, 116 | "output_type": "execute_result" 117 | } 118 | ], 119 | "source": [ 120 | "$result = $fmdb->person_layout->query();" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "The 'httpStatus()' method returns the HTTP status code in the latest response." 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 5, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "HTTP Status: 200\n" 139 | ] 140 | }, 141 | "execution_count": 5, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "echo \"HTTP Status: {$fmdb->httpStatus()}\";" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "The following two methods return the error code and message of the latest API call which is submitted in query() method. You can check API calling succeed or fail if error code is or isn't 0 every after API calling methods." 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 6, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "data": { 164 | "text/plain": [ 165 | "Error Code: 0\n" 166 | ] 167 | }, 168 | "execution_count": 6, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | }, 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "Error Message: \n" 176 | ] 177 | }, 178 | "execution_count": 6, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "echo \"Error Code: {$fmdb->errorCode()}\";\n", 185 | "echo \"Error Message: {$fmdb->errorMessage()}\";" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "### Accessing Queried Data\n", 193 | "The FileMakerRelation class implements the Iterator interface and it can repeat with 'foreach.' The \\$record also refers a FileMakerRelation object but it is for single record. This layout has fields as like 'id', 'name', 'mail' and so on, and the field name can be handle as a property name of the the record referring with \\$record. The 'person' table in database has 3 records and you can see 3 outs below. The recordId or redId is required to update the record, and it can get by the getRecordId method." 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 7, 199 | "metadata": {}, 200 | "outputs": [ 201 | { 202 | "data": { 203 | "text/plain": [ 204 | "[id]1, [name]Masayuki Nii, [mail]Dog\n" 205 | ] 206 | }, 207 | "execution_count": 7, 208 | "metadata": {}, 209 | "output_type": "execute_result" 210 | }, 211 | { 212 | "data": { 213 | "text/plain": [ 214 | "__[recordId = 1]\n" 215 | ] 216 | }, 217 | "execution_count": 7, 218 | "metadata": {}, 219 | "output_type": "execute_result" 220 | }, 221 | { 222 | "data": { 223 | "text/plain": [ 224 | "[id]2, [name]Someone, [mail]msyk@msyk.net\n" 225 | ] 226 | }, 227 | "execution_count": 7, 228 | "metadata": {}, 229 | "output_type": "execute_result" 230 | }, 231 | { 232 | "data": { 233 | "text/plain": [ 234 | "__[recordId = 2]\n" 235 | ] 236 | }, 237 | "execution_count": 7, 238 | "metadata": {}, 239 | "output_type": "execute_result" 240 | }, 241 | { 242 | "data": { 243 | "text/plain": [ 244 | "[id]3, [name]Anyone, [mail]Dog\n" 245 | ] 246 | }, 247 | "execution_count": 7, 248 | "metadata": {}, 249 | "output_type": "execute_result" 250 | }, 251 | { 252 | "data": { 253 | "text/plain": [ 254 | "__[recordId = 3]\n" 255 | ] 256 | }, 257 | "execution_count": 7, 258 | "metadata": {}, 259 | "output_type": "execute_result" 260 | } 261 | ], 262 | "source": [ 263 | "if (!is_null($result)) {\n", 264 | " foreach ($result as $record) {\n", 265 | " echo \"[id]{$record->id}, [name]{$record->name}, [mail]{$record->mail}\";\n", 266 | " echo \"__[recordId = {$record->getRecordId()}]\";\n", 267 | " }\n", 268 | "}" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "### Accessing to Portal Data\n", 276 | "A portal name property returns records of portal as FileMakerRelation object. You can repeat with foreach for the portal records.\n", 277 | "\n", 278 | "Technically portal field has to be refered as \"contact_to::id\" but it can be an indentifier in PHP. In this case you can call field method as like 'field(\"summary\", \"contact_to\").' If the field belongs to the table occurrence for the portal, you can refer the field as like '$item->id.' If the field belongs to another table occurrence, you have to call the 'field()' method.\n", 279 | "\n", 280 | "If the object name of the portal is blank, it can be referred as the table occurrence name. If the object name is specified, you have to access with the object name and it means you have to call 'field()' method to get the value." 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 8, 286 | "metadata": {}, 287 | "outputs": [ 288 | { 289 | "data": { 290 | "text/plain": [ 291 | "id: 1, name: Masayuki Nii, mail: Dog\n" 292 | ] 293 | }, 294 | "execution_count": 8, 295 | "metadata": {}, 296 | "output_type": "execute_result" 297 | }, 298 | { 299 | "data": { 300 | "text/plain": [ 301 | "__[PORTAL(contact_to)][id]1, [summary]Telephone\n" 302 | ] 303 | }, 304 | "execution_count": 8, 305 | "metadata": {}, 306 | "output_type": "execute_result" 307 | }, 308 | { 309 | "data": { 310 | "text/plain": [ 311 | "__[PORTAL(contact_to)][id]2, [summary]Meetings\n" 312 | ] 313 | }, 314 | "execution_count": 8, 315 | "metadata": {}, 316 | "output_type": "execute_result" 317 | }, 318 | { 319 | "data": { 320 | "text/plain": [ 321 | "__[PORTAL(contact_to)][id]3, [summary]Mail\n" 322 | ] 323 | }, 324 | "execution_count": 8, 325 | "metadata": {}, 326 | "output_type": "execute_result" 327 | }, 328 | { 329 | "data": { 330 | "text/plain": [ 331 | "id: 2, name: Someone, mail: msyk@msyk.net\n" 332 | ] 333 | }, 334 | "execution_count": 8, 335 | "metadata": {}, 336 | "output_type": "execute_result" 337 | }, 338 | { 339 | "data": { 340 | "text/plain": [ 341 | "__[PORTAL(contact_to)][id]4, [summary]Calling\n" 342 | ] 343 | }, 344 | "execution_count": 8, 345 | "metadata": {}, 346 | "output_type": "execute_result" 347 | }, 348 | { 349 | "data": { 350 | "text/plain": [ 351 | "__[PORTAL(contact_to)][id]5, [summary]Telephone\n" 352 | ] 353 | }, 354 | "execution_count": 8, 355 | "metadata": {}, 356 | "output_type": "execute_result" 357 | }, 358 | { 359 | "data": { 360 | "text/plain": [ 361 | "id: 3, name: Anyone, mail: Dog\n" 362 | ] 363 | }, 364 | "execution_count": 8, 365 | "metadata": {}, 366 | "output_type": "execute_result" 367 | }, 368 | { 369 | "data": { 370 | "text/plain": [ 371 | "__[PORTAL(contact_to)][id]6, [summary]Meeting\n" 372 | ] 373 | }, 374 | "execution_count": 8, 375 | "metadata": {}, 376 | "output_type": "execute_result" 377 | }, 378 | { 379 | "data": { 380 | "text/plain": [ 381 | "__[PORTAL(contact_to)][id]7, [summary]Mail etcsss\n" 382 | ] 383 | }, 384 | "execution_count": 8, 385 | "metadata": {}, 386 | "output_type": "execute_result" 387 | } 388 | ], 389 | "source": [ 390 | "if (!is_null($result)) {\n", 391 | " foreach ($result as $record) {\n", 392 | " echo \"id: {$record->id}, name: {$record->name}, mail: {$record->mail}\";\n", 393 | " $contacts = $record->Contact;\n", 394 | " foreach ($contacts as $item) {\n", 395 | " $id = $item->field(\"id\", \"contact_to\");\n", 396 | " $summary = $item->field(\"summary\", \"contact_to\");\n", 397 | " echo \"__[PORTAL(contact_to)][id]{$id}, [summary]{$summary}\";\n", 398 | " }\n", 399 | " }\n", 400 | "}" 401 | ] 402 | }, 403 | { 404 | "cell_type": "markdown", 405 | "metadata": {}, 406 | "source": [ 407 | "The FileMakerRelation object from 'query()' method can be accessed as like the 'cursor' style repeating.\n", 408 | "The 'count()' method returns the number of records in response. The variable $result referes current record and you can get the field value with the propaty having the same field name.\n", 409 | "The portal can be done with same way. The 'next()' method steps forward the pointer of current record.\n", 410 | "Before examining the cursor looping, the pointer has to move to the first record with the rewind() method." 411 | ] 412 | }, 413 | { 414 | "cell_type": "code", 415 | "execution_count": 9, 416 | "metadata": {}, 417 | "outputs": [ 418 | { 419 | "data": { 420 | "text/plain": [ 421 | "[id]1, [name]Masayuki Nii, [mail]Dog\n" 422 | ] 423 | }, 424 | "execution_count": 9, 425 | "metadata": {}, 426 | "output_type": "execute_result" 427 | }, 428 | { 429 | "data": { 430 | "text/plain": [ 431 | "__[PORTAL(contact_to)][id]1, [summary]Telephone\n" 432 | ] 433 | }, 434 | "execution_count": 9, 435 | "metadata": {}, 436 | "output_type": "execute_result" 437 | }, 438 | { 439 | "data": { 440 | "text/plain": [ 441 | "__[PORTAL(contact_to)][id]2, [summary]Meetings\n" 442 | ] 443 | }, 444 | "execution_count": 9, 445 | "metadata": {}, 446 | "output_type": "execute_result" 447 | }, 448 | { 449 | "data": { 450 | "text/plain": [ 451 | "__[PORTAL(contact_to)][id]3, [summary]Mail\n" 452 | ] 453 | }, 454 | "execution_count": 9, 455 | "metadata": {}, 456 | "output_type": "execute_result" 457 | }, 458 | { 459 | "data": { 460 | "text/plain": [ 461 | "__[PORTAL(history_to)][startdate]04/01/2001, [enddate]03/31/2003\n" 462 | ] 463 | }, 464 | "execution_count": 9, 465 | "metadata": {}, 466 | "output_type": "execute_result" 467 | }, 468 | { 469 | "data": { 470 | "text/plain": [ 471 | "__[PORTAL(history_to)][startdate]04/01/2003, [enddate]03/31/2007\n" 472 | ] 473 | }, 474 | "execution_count": 9, 475 | "metadata": {}, 476 | "output_type": "execute_result" 477 | }, 478 | { 479 | "data": { 480 | "text/plain": [ 481 | "[id]2, [name]Someone, [mail]msyk@msyk.net\n" 482 | ] 483 | }, 484 | "execution_count": 9, 485 | "metadata": {}, 486 | "output_type": "execute_result" 487 | }, 488 | { 489 | "data": { 490 | "text/plain": [ 491 | "__[PORTAL(contact_to)][id]4, [summary]Calling\n" 492 | ] 493 | }, 494 | "execution_count": 9, 495 | "metadata": {}, 496 | "output_type": "execute_result" 497 | }, 498 | { 499 | "data": { 500 | "text/plain": [ 501 | "__[PORTAL(contact_to)][id]5, [summary]Telephone\n" 502 | ] 503 | }, 504 | "execution_count": 9, 505 | "metadata": {}, 506 | "output_type": "execute_result" 507 | }, 508 | { 509 | "data": { 510 | "text/plain": [ 511 | "[id]3, [name]Anyone, [mail]Dog\n" 512 | ] 513 | }, 514 | "execution_count": 9, 515 | "metadata": {}, 516 | "output_type": "execute_result" 517 | }, 518 | { 519 | "data": { 520 | "text/plain": [ 521 | "__[PORTAL(contact_to)][id]6, [summary]Meeting\n" 522 | ] 523 | }, 524 | "execution_count": 9, 525 | "metadata": {}, 526 | "output_type": "execute_result" 527 | }, 528 | { 529 | "data": { 530 | "text/plain": [ 531 | "__[PORTAL(contact_to)][id]7, [summary]Mail etcsss\n" 532 | ] 533 | }, 534 | "execution_count": 9, 535 | "metadata": {}, 536 | "output_type": "execute_result" 537 | } 538 | ], 539 | "source": [ 540 | "$result->rewind();\n", 541 | "for ($i = 0; $i < $result->count(); $i++) {\n", 542 | " echo \"[id]{$result->id}, [name]{$result->name}, [mail]{$result->mail}\";\n", 543 | " $contacts = $result->Contact;\n", 544 | " $contacts->rewind();\n", 545 | " for ($j = 0; $j < $contacts->count(); $j++) {\n", 546 | " $id = $contacts->field(\"id\", \"contact_to\");\n", 547 | " $summary = $contacts->field(\"summary\", \"contact_to\");\n", 548 | " echo \"__[PORTAL(contact_to)][id]{$id}, [summary]{$summary}\";\n", 549 | " $contacts->next();\n", 550 | " }\n", 551 | " $histories = $result->History;\n", 552 | " $histories->rewind();\n", 553 | " for ($j = 0; $j < $histories->count(); $j++) {\n", 554 | " $std = $histories->field(\"startdate\", \"history_to\");\n", 555 | " $endd = $histories->field(\"enddate\", \"history_to\");\n", 556 | " echo \"__[PORTAL(history_to)][startdate]{$std}, [enddate]{$endd}\";\n", 557 | " $histories->next();\n", 558 | " }\n", 559 | " $result->next();\n", 560 | "}" 561 | ] 562 | }, 563 | { 564 | "cell_type": "markdown", 565 | "metadata": {}, 566 | "source": [ 567 | "### Sort Fields\n", 568 | "The second parameter of query method specifies fields for sorting with array of array data. The innner array has one or two elements with the field name and the dicrection as below. This menas ordering by the id field decendly." 569 | ] 570 | }, 571 | { 572 | "cell_type": "code", 573 | "execution_count": 10, 574 | "metadata": {}, 575 | "outputs": [ 576 | { 577 | "data": { 578 | "text/plain": [ 579 | "[id]3, [name]Anyone, [mail] Dog\n" 580 | ] 581 | }, 582 | "execution_count": 10, 583 | "metadata": {}, 584 | "output_type": "execute_result" 585 | }, 586 | { 587 | "data": { 588 | "text/plain": [ 589 | "[id]2, [name]Someone, [mail] msyk@msyk.net\n" 590 | ] 591 | }, 592 | "execution_count": 10, 593 | "metadata": {}, 594 | "output_type": "execute_result" 595 | }, 596 | { 597 | "data": { 598 | "text/plain": [ 599 | "[id]1, [name]Masayuki Nii, [mail] Dog\n" 600 | ] 601 | }, 602 | "execution_count": 10, 603 | "metadata": {}, 604 | "output_type": "execute_result" 605 | } 606 | ], 607 | "source": [ 608 | "$result = $fmdb->person_layout->query(null, [[\"id\", \"descend\"]]);\n", 609 | "if (!is_null($result)) {\n", 610 | " foreach ($result as $record) {\n", 611 | " echo \"[id]{$record->id}, [name]{$record->name}, [mail] {$record->mail}\";\n", 612 | " }\n", 613 | "}" 614 | ] 615 | }, 616 | { 617 | "cell_type": "markdown", 618 | "metadata": {}, 619 | "source": [ 620 | "### Start Record and Limit Records\n", 621 | "The third and fourth parameter of the query method specify the start record number and the limit number of record. The following query means query 5 records from 20th record with ordered by the f3 fields." 622 | ] 623 | }, 624 | { 625 | "cell_type": "code", 626 | "execution_count": 11, 627 | "metadata": {}, 628 | "outputs": [ 629 | { 630 | "data": { 631 | "text/plain": [ 632 | "[postal code]1000400, [place]東京都新島村以下に掲載がない場合\n" 633 | ] 634 | }, 635 | "execution_count": 11, 636 | "metadata": {}, 637 | "output_type": "execute_result" 638 | }, 639 | { 640 | "data": { 641 | "text/plain": [ 642 | "[postal code]1000401, [place]東京都新島村若郷\n" 643 | ] 644 | }, 645 | "execution_count": 11, 646 | "metadata": {}, 647 | "output_type": "execute_result" 648 | }, 649 | { 650 | "data": { 651 | "text/plain": [ 652 | "[postal code]1000402, [place]東京都新島村本村\n" 653 | ] 654 | }, 655 | "execution_count": 11, 656 | "metadata": {}, 657 | "output_type": "execute_result" 658 | }, 659 | { 660 | "data": { 661 | "text/plain": [ 662 | "[postal code]1000511, [place]東京都新島村式根島\n" 663 | ] 664 | }, 665 | "execution_count": 11, 666 | "metadata": {}, 667 | "output_type": "execute_result" 668 | }, 669 | { 670 | "data": { 671 | "text/plain": [ 672 | "[postal code]1000601, [place]東京都神津島村神津島村一円\n" 673 | ] 674 | }, 675 | "execution_count": 11, 676 | "metadata": {}, 677 | "output_type": "execute_result" 678 | } 679 | ], 680 | "source": [ 681 | "$result = $fmdb->postalcode->query(null, [[\"f3\"]], 20, 5);\n", 682 | "if (!is_null($result)) {\n", 683 | " foreach ($result as $record) {\n", 684 | " echo \"[postal code]{$record->f3}, [place]{$record->f7}{$record->f8}{$record->f9}\";\n", 685 | " }\n", 686 | "}" 687 | ] 688 | }, 689 | { 690 | "cell_type": "markdown", 691 | "metadata": {}, 692 | "source": [ 693 | "### Query with Condtion\n", 694 | "The 'query()' method can have several parameters. The first parameter specifies condtions with array of array. Simply key is a field name and value is the value. The following condition means just query the id field is \"1\"." 695 | ] 696 | }, 697 | { 698 | "cell_type": "code", 699 | "execution_count": 12, 700 | "metadata": {}, 701 | "outputs": [ 702 | { 703 | "data": { 704 | "text/plain": [ 705 | "[id]1, [name]Masayuki Nii, [mail] Dog\n" 706 | ] 707 | }, 708 | "execution_count": 12, 709 | "metadata": {}, 710 | "output_type": "execute_result" 711 | } 712 | ], 713 | "source": [ 714 | "$result = $fmdb->person_layout->query([[\"id\" => \"1\"]]);\n", 715 | "if (!is_null($result)) {\n", 716 | " foreach ($result as $record) {\n", 717 | " echo \"[id]{$record->id}, [name]{$record->name}, [mail] {$record->mail}\";\n", 718 | " }\n", 719 | "}" 720 | ] 721 | }, 722 | { 723 | "cell_type": "markdown", 724 | "metadata": {}, 725 | "source": [ 726 | "Next query means character 中 contains in the f9 field." 727 | ] 728 | }, 729 | { 730 | "cell_type": "code", 731 | "execution_count": 13, 732 | "metadata": {}, 733 | "outputs": [ 734 | { 735 | "data": { 736 | "text/plain": [ 737 | "[postal code]1030008, [place]東京都中央区日本橋中洲\n" 738 | ] 739 | }, 740 | "execution_count": 13, 741 | "metadata": {}, 742 | "output_type": "execute_result" 743 | }, 744 | { 745 | "data": { 746 | "text/plain": [ 747 | "[postal code]1610035, [place]東京都新宿区中井\n" 748 | ] 749 | }, 750 | "execution_count": 13, 751 | "metadata": {}, 752 | "output_type": "execute_result" 753 | }, 754 | { 755 | "data": { 756 | "text/plain": [ 757 | "[postal code]1610032, [place]東京都新宿区中落合\n" 758 | ] 759 | }, 760 | "execution_count": 13, 761 | "metadata": {}, 762 | "output_type": "execute_result" 763 | } 764 | ], 765 | "source": [ 766 | "$result = $fmdb->postalcode->query([[\"f9\" => \"中\"]], null, 1, 3);\n", 767 | "if (!is_null($result)) {\n", 768 | " foreach ($result as $record) {\n", 769 | " echo \"[postal code]{$record->f3}, [place]{$record->f7}{$record->f8}{$record->f9}\";\n", 770 | " }\n", 771 | "}" 772 | ] 773 | }, 774 | { 775 | "cell_type": "markdown", 776 | "metadata": {}, 777 | "source": [ 778 | "Next query means the f8 field is just 中央区 AND character 中 contains in the f9 field." 779 | ] 780 | }, 781 | { 782 | "cell_type": "code", 783 | "execution_count": 14, 784 | "metadata": {}, 785 | "outputs": [ 786 | { 787 | "data": { 788 | "text/plain": [ 789 | "[postal code]1030008, [place]東京都中央区日本橋中洲\n" 790 | ] 791 | }, 792 | "execution_count": 14, 793 | "metadata": {}, 794 | "output_type": "execute_result" 795 | } 796 | ], 797 | "source": [ 798 | "$result = $fmdb->postalcode->query([[\"f8\" => \"=中央区\", \"f9\" => \"中\"]], null, 1, 3);\n", 799 | "if (!is_null($result)) {\n", 800 | " foreach ($result as $record) {\n", 801 | " echo \"[postal code]{$record->f3}, [place]{$record->f7}{$record->f8}{$record->f9}\";\n", 802 | " }\n", 803 | "}" 804 | ] 805 | }, 806 | { 807 | "cell_type": "markdown", 808 | "metadata": {}, 809 | "source": [ 810 | "Next query means the character 中 contains in the f8 OR f9 field." 811 | ] 812 | }, 813 | { 814 | "cell_type": "code", 815 | "execution_count": 15, 816 | "metadata": {}, 817 | "outputs": [ 818 | { 819 | "data": { 820 | "text/plain": [ 821 | "[postal code]1030000, [place]東京都中央区以下に掲載がない場合\n" 822 | ] 823 | }, 824 | "execution_count": 15, 825 | "metadata": {}, 826 | "output_type": "execute_result" 827 | }, 828 | { 829 | "data": { 830 | "text/plain": [ 831 | "[postal code]1040044, [place]東京都中央区明石町\n" 832 | ] 833 | }, 834 | "execution_count": 15, 835 | "metadata": {}, 836 | "output_type": "execute_result" 837 | }, 838 | { 839 | "data": { 840 | "text/plain": [ 841 | "[postal code]1040042, [place]東京都中央区入船\n" 842 | ] 843 | }, 844 | "execution_count": 15, 845 | "metadata": {}, 846 | "output_type": "execute_result" 847 | } 848 | ], 849 | "source": [ 850 | "$result = $fmdb->postalcode->query([[\"f8\" => \"中\"], [\"f9\" => \"中\"]], null, 1, 3);\n", 851 | "if (!is_null($result)) {\n", 852 | " foreach ($result as $record) {\n", 853 | " echo \"[postal code]{$record->f3}, [place]{$record->f7}{$record->f8}{$record->f9}\";\n", 854 | " }\n", 855 | "}" 856 | ] 857 | }, 858 | { 859 | "cell_type": "markdown", 860 | "metadata": {}, 861 | "source": [ 862 | "Next query means the f3 field is within the range from 170000 to 171000." 863 | ] 864 | }, 865 | { 866 | "cell_type": "code", 867 | "execution_count": 16, 868 | "metadata": {}, 869 | "outputs": [ 870 | { 871 | "data": { 872 | "text/plain": [ 873 | "[postal code]1700000, [place]東京都豊島区以下に掲載がない場合\n" 874 | ] 875 | }, 876 | "execution_count": 16, 877 | "metadata": {}, 878 | "output_type": "execute_result" 879 | }, 880 | { 881 | "data": { 882 | "text/plain": [ 883 | "[postal code]1700014, [place]東京都豊島区池袋(1丁目)\n" 884 | ] 885 | }, 886 | "execution_count": 16, 887 | "metadata": {}, 888 | "output_type": "execute_result" 889 | }, 890 | { 891 | "data": { 892 | "text/plain": [ 893 | "[postal code]1700011, [place]東京都豊島区池袋本町\n" 894 | ] 895 | }, 896 | "execution_count": 16, 897 | "metadata": {}, 898 | "output_type": "execute_result" 899 | } 900 | ], 901 | "source": [ 902 | "$result = $fmdb->postalcode->query([[\"f3\" => \"1700000...1710000\"]], null, 1, 3);\n", 903 | "if (!is_null($result)) {\n", 904 | " foreach ($result as $record) {\n", 905 | " echo \"[postal code]{$record->f3}, [place]{$record->f7}{$record->f8}{$record->f9}\";\n", 906 | " }\n", 907 | "}" 908 | ] 909 | }, 910 | { 911 | "cell_type": "markdown", 912 | "metadata": {}, 913 | "source": [ 914 | "### Restricting to Specified Portals\n", 915 | "The portal specification (the 5th parameter of query or the second parameter of getRecord) has to be an array with the object name of the portal not the table occurrence name. The person_layout has two portals named \"Contact\" and \"History\", but if the 5th parameter is specified, portal data in the result is just in the parameter." 916 | ] 917 | }, 918 | { 919 | "cell_type": "code", 920 | "execution_count": 17, 921 | "metadata": {}, 922 | "outputs": [ 923 | { 924 | "data": { 925 | "text/plain": [ 926 | "[id]1, [name]Masayuki Nii, [mail]Dog\n" 927 | ] 928 | }, 929 | "execution_count": 17, 930 | "metadata": {}, 931 | "output_type": "execute_result" 932 | }, 933 | { 934 | "data": { 935 | "text/plain": [ 936 | "__[PORTAL(history_to)][startdate]04/01/2001, [enddate]03/31/2003\n" 937 | ] 938 | }, 939 | "execution_count": 17, 940 | "metadata": {}, 941 | "output_type": "execute_result" 942 | }, 943 | { 944 | "data": { 945 | "text/plain": [ 946 | "__[PORTAL(history_to)][startdate]04/01/2003, [enddate]03/31/2007\n" 947 | ] 948 | }, 949 | "execution_count": 17, 950 | "metadata": {}, 951 | "output_type": "execute_result" 952 | } 953 | ], 954 | "source": [ 955 | "$result = $fmdb->person_layout->query([[\"id\" => \"1\"]], null, 1, -1, [\"History\"]);\n", 956 | "if (!is_null($result)) {\n", 957 | " foreach ($result as $record) {\n", 958 | " echo \"[id]{$record->id}, [name]{$record->name}, [mail]{$record->mail}\";\n", 959 | " $histories = $record->History;\n", 960 | " foreach ($histories as $item) {\n", 961 | " $startdate = $item->field(\"startdate\", \"history_to\");\n", 962 | " $enddate = $item->field(\"enddate\", \"history_to\");\n", 963 | " echo \"__[PORTAL(history_to)][startdate]{$startdate}, [enddate]{$enddate}\";\n", 964 | " }\n", 965 | " }\n", 966 | "}" 967 | ] 968 | }, 969 | { 970 | "cell_type": "markdown", 971 | "metadata": {}, 972 | "source": [ 973 | "## Writing Operations\n", 974 | "### Create Record\n", 975 | "The 'create()' method creates a record with values in parameter.\n", 976 | "The associated array of the parameter has to be a series of field name key and its value." 977 | ] 978 | }, 979 | { 980 | "cell_type": "code", 981 | "execution_count": 18, 982 | "metadata": {}, 983 | "outputs": [ 984 | { 985 | "data": { 986 | "text/plain": [ 987 | "\"\u001b[32m4123\u001b[39m\"" 988 | ] 989 | }, 990 | "execution_count": 18, 991 | "metadata": {}, 992 | "output_type": "execute_result" 993 | } 994 | ], 995 | "source": [ 996 | "$recId = $fmdb->postalcode->create([\"f3\" => \"field 3 data\", \"f7\" => \"field 7 data\"]);" 997 | ] 998 | }, 999 | { 1000 | "cell_type": "markdown", 1001 | "metadata": {}, 1002 | "source": [ 1003 | "The 'getRecord()' method query the record with the recordId of the parameter.\n", 1004 | "It returns the FileMakerRelation object and you can handle it with the return value from 'query()' method." 1005 | ] 1006 | }, 1007 | { 1008 | "cell_type": "code", 1009 | "execution_count": 19, 1010 | "metadata": {}, 1011 | "outputs": [ 1012 | { 1013 | "data": { 1014 | "text/plain": [ 1015 | "[f3]field 3 data, [f7]field 7 data, [f8]\n" 1016 | ] 1017 | }, 1018 | "execution_count": 19, 1019 | "metadata": {}, 1020 | "output_type": "execute_result" 1021 | } 1022 | ], 1023 | "source": [ 1024 | "$result = $fmdb->postalcode->getRecord($recId);\n", 1025 | "if (!is_null($result)) {\n", 1026 | " foreach ($result as $record) {\n", 1027 | " echo \"[f3]{$record->f3}, [f7]{$record->f7}, [f8]{$record->f8}\";\n", 1028 | " }\n", 1029 | "}" 1030 | ] 1031 | }, 1032 | { 1033 | "cell_type": "markdown", 1034 | "metadata": {}, 1035 | "source": [ 1036 | "### Update Record\n", 1037 | "The 'update()' method modifies fields in a record. You have to set parameters as the recordId of target record and associated array to specify the modified data." 1038 | ] 1039 | }, 1040 | { 1041 | "cell_type": "code", 1042 | "execution_count": 20, 1043 | "metadata": {}, 1044 | "outputs": [ 1045 | { 1046 | "data": { 1047 | "text/plain": [ 1048 | "[f3]field 3 modifed, [f7]field 7 data, [f8]field 8 update\n" 1049 | ] 1050 | }, 1051 | "execution_count": 20, 1052 | "metadata": {}, 1053 | "output_type": "execute_result" 1054 | } 1055 | ], 1056 | "source": [ 1057 | "$fmdb->postalcode->update($recId, [\"f3\" => \"field 3 modifed\", \"f8\" => \"field 8 update\"]);\n", 1058 | "$result = $fmdb->postalcode->getRecord($recId);\n", 1059 | "if (!is_null($result)) {\n", 1060 | " foreach ($result as $record) {\n", 1061 | " echo \"[f3]{$record->f3}, [f7]{$record->f7}, [f8]{$record->f8}\";\n", 1062 | " }\n", 1063 | "}" 1064 | ] 1065 | }, 1066 | { 1067 | "cell_type": "markdown", 1068 | "metadata": {}, 1069 | "source": [ 1070 | "## Delete Record\n", 1071 | "The 'delete()' method deletes the record specified by the parameter." 1072 | ] 1073 | }, 1074 | { 1075 | "cell_type": "code", 1076 | "execution_count": 21, 1077 | "metadata": {}, 1078 | "outputs": [ 1079 | { 1080 | "data": { 1081 | "text/plain": [ 1082 | "\u001b[36mnull\u001b[39m" 1083 | ] 1084 | }, 1085 | "execution_count": 21, 1086 | "metadata": {}, 1087 | "output_type": "execute_result" 1088 | } 1089 | ], 1090 | "source": [ 1091 | "$fmdb->postalcode->delete($recId);" 1092 | ] 1093 | }, 1094 | { 1095 | "cell_type": "markdown", 1096 | "metadata": {}, 1097 | "source": [ 1098 | "## Call Script\n", 1099 | "Some methods ex. query can execute a script with 6th paramter." 1100 | ] 1101 | }, 1102 | { 1103 | "cell_type": "markdown", 1104 | "metadata": {}, 1105 | "source": [ 1106 | "This example execute the \"TextScript\" script with the parameter \"ok\". The script finishes no error and returns value and you can detect it from the getScriptResult method." 1107 | ] 1108 | }, 1109 | { 1110 | "cell_type": "code", 1111 | "execution_count": 22, 1112 | "metadata": {}, 1113 | "outputs": [ 1114 | { 1115 | "data": { 1116 | "text/plain": [ 1117 | "Script Error: 0\n" 1118 | ] 1119 | }, 1120 | "execution_count": 22, 1121 | "metadata": {}, 1122 | "output_type": "execute_result" 1123 | }, 1124 | { 1125 | "data": { 1126 | "text/plain": [ 1127 | "Script Result: It's over.\n" 1128 | ] 1129 | }, 1130 | "execution_count": 22, 1131 | "metadata": {}, 1132 | "output_type": "execute_result" 1133 | } 1134 | ], 1135 | "source": [ 1136 | "$scriptSpec = [\"script\" => \"TestScript\", \"script.param\" => \"ok\"];\n", 1137 | "$result = $fmdb->person_layout->query(null, null, -1, 1, null, $scriptSpec);\n", 1138 | "if (!is_null($result)) {\n", 1139 | " echo \"Script Error: {$fmdb->person_layout->getScriptError()}\";\n", 1140 | " echo \"Script Result: {$fmdb->person_layout->getScriptResult()}\";\n", 1141 | "}" 1142 | ] 1143 | }, 1144 | { 1145 | "cell_type": "markdown", 1146 | "metadata": {}, 1147 | "source": [ 1148 | "The \"script\" key's script executes just after querying. Otherwise the \"script.prerequest\" key's one does before querying. Errors and/or Result can be detect with methods described below." 1149 | ] 1150 | }, 1151 | { 1152 | "cell_type": "code", 1153 | "execution_count": 23, 1154 | "metadata": {}, 1155 | "outputs": [ 1156 | { 1157 | "data": { 1158 | "text/plain": [ 1159 | "Script Error: 0\n" 1160 | ] 1161 | }, 1162 | "execution_count": 23, 1163 | "metadata": {}, 1164 | "output_type": "execute_result" 1165 | }, 1166 | { 1167 | "data": { 1168 | "text/plain": [ 1169 | "Script Result: It's over.\n" 1170 | ] 1171 | }, 1172 | "execution_count": 23, 1173 | "metadata": {}, 1174 | "output_type": "execute_result" 1175 | } 1176 | ], 1177 | "source": [ 1178 | "$scriptSpec = [\"script.prerequest\" => \"TestScript\", \"script.prerequest.param\" => \"ok\"];\n", 1179 | "$result = $fmdb->person_layout->query(null, null, -1, 1, null, $scriptSpec);\n", 1180 | "if (!is_null($result)) {\n", 1181 | " echo \"Script Error: {$fmdb->person_layout->getScriptErrorPrerequest()}\";\n", 1182 | " echo \"Script Result: {$fmdb->person_layout->getScriptResultPrerequest()}\";\n", 1183 | "\n", 1184 | "}" 1185 | ] 1186 | }, 1187 | { 1188 | "cell_type": "markdown", 1189 | "metadata": {}, 1190 | "source": [ 1191 | "If any errors happens in the script, the error is not 0 as shown below." 1192 | ] 1193 | }, 1194 | { 1195 | "cell_type": "code", 1196 | "execution_count": 24, 1197 | "metadata": {}, 1198 | "outputs": [ 1199 | { 1200 | "data": { 1201 | "text/plain": [ 1202 | "Script Error: 102\n" 1203 | ] 1204 | }, 1205 | "execution_count": 24, 1206 | "metadata": {}, 1207 | "output_type": "execute_result" 1208 | }, 1209 | { 1210 | "data": { 1211 | "text/plain": [ 1212 | "Script Result: It's error.\n" 1213 | ] 1214 | }, 1215 | "execution_count": 24, 1216 | "metadata": {}, 1217 | "output_type": "execute_result" 1218 | } 1219 | ], 1220 | "source": [ 1221 | "$scriptSpec = [\"script\" => \"TestScript\", \"script.param\" => \"not\"];\n", 1222 | "$result = $fmdb->person_layout->query(null, null, -1, 1, null, $scriptSpec);\n", 1223 | "if (!is_null($result)) {\n", 1224 | " echo \"Script Error: {$fmdb->person_layout->getScriptError()}\";\n", 1225 | " echo \"Script Result: {$fmdb->person_layout->getScriptResult()}\";\n", 1226 | "}" 1227 | ] 1228 | }, 1229 | { 1230 | "cell_type": "markdown", 1231 | "metadata": {}, 1232 | "source": [ 1233 | "If you don't specify any script information, error and result are both \"blank\" data." 1234 | ] 1235 | }, 1236 | { 1237 | "cell_type": "code", 1238 | "execution_count": 25, 1239 | "metadata": {}, 1240 | "outputs": [ 1241 | { 1242 | "data": { 1243 | "text/plain": [ 1244 | "Script Error: \n" 1245 | ] 1246 | }, 1247 | "execution_count": 25, 1248 | "metadata": {}, 1249 | "output_type": "execute_result" 1250 | }, 1251 | { 1252 | "data": { 1253 | "text/plain": [ 1254 | "Script Result: \n" 1255 | ] 1256 | }, 1257 | "execution_count": 25, 1258 | "metadata": {}, 1259 | "output_type": "execute_result" 1260 | } 1261 | ], 1262 | "source": [ 1263 | "$result = $fmdb->person_layout->query(null, null, -1, 1);\n", 1264 | "if (!is_null($result)) {\n", 1265 | " echo \"Script Error: {$fmdb->person_layout->getScriptError()}\";\n", 1266 | " echo \"Script Result: {$fmdb->person_layout->getScriptResult()}\";\n", 1267 | "}" 1268 | ] 1269 | }, 1270 | { 1271 | "cell_type": "markdown", 1272 | "metadata": {}, 1273 | "source": [ 1274 | "## File Uploading\n", 1275 | "A new record is created in \"testtable\" table on the first statement of below.\n", 1276 | "Then the \"testtable\" table has a container filed \"vc1\". One image file is going to be uploaded to it.\n", 1277 | "The file path, record id and field name are required." 1278 | ] 1279 | }, 1280 | { 1281 | "cell_type": "code", 1282 | "execution_count": 26, 1283 | "metadata": {}, 1284 | "outputs": [ 1285 | { 1286 | "data": { 1287 | "text/plain": [ 1288 | "\u001b[36mnull\u001b[39m" 1289 | ] 1290 | }, 1291 | "execution_count": 26, 1292 | "metadata": {}, 1293 | "output_type": "execute_result" 1294 | } 1295 | ], 1296 | "source": [ 1297 | "$recId = $fmdb->testtable->create();\n", 1298 | "$fmdb->testtable->uploadFile(\"samples/cat.jpg\", $recId, \"vc1\");" 1299 | ] 1300 | }, 1301 | { 1302 | "cell_type": "markdown", 1303 | "metadata": {}, 1304 | "source": [ 1305 | "What kind of data does the container field which inserted an image return? For example, the returned value is going to show as the value of the vc1 field as below. It'a kind of url, and it can get the content of the container field, and it means you can download with the getContainerData method. " 1306 | ] 1307 | }, 1308 | { 1309 | "cell_type": "code", 1310 | "execution_count": 27, 1311 | "metadata": {}, 1312 | "outputs": [ 1313 | { 1314 | "data": { 1315 | "text/plain": [ 1316 | "vc1: https://localhost/Streaming_SSL/MainDB/F18E548D6339DB444ED92BF13DE220A5F773E7E68607E29E388FBD6ECE1B5AEF.jpg?RCType=EmbeddedRCFileProcessor\n" 1317 | ] 1318 | }, 1319 | "execution_count": 27, 1320 | "metadata": {}, 1321 | "output_type": "execute_result" 1322 | }, 1323 | { 1324 | "data": { 1325 | "text/plain": [ 1326 | "\u001b[31;1mException with message 'Error in creating cookie file. Failed to connect to localhost port 443: Connection refused'\u001b[39;22m" 1327 | ] 1328 | }, 1329 | "execution_count": 27, 1330 | "metadata": {}, 1331 | "output_type": "execute_result" 1332 | } 1333 | ], 1334 | "source": [ 1335 | "$result = $fmdb->testtable->getRecord($recId);\n", 1336 | "if(!is_null($result)) {\n", 1337 | " foreach ($result as $record) {\n", 1338 | " echo \"vc1: {$record->vc1}\";\n", 1339 | " echo \"\";\n", 1340 | " }\n", 1341 | "}" 1342 | ] 1343 | }, 1344 | { 1345 | "cell_type": "markdown", 1346 | "metadata": {}, 1347 | "source": [ 1348 | "## Batch Operations\n", 1349 | "If you call the 'startCommunication()' method, you can describe a series of database operation\n", 1350 | "calls. This means the authentication is going to be done at the 'startCommunication()' method, and the token is going to be shared with following statements. The 'endCommunication()' calls logout REST API call and invalidate the shared token." 1351 | ] 1352 | }, 1353 | { 1354 | "cell_type": "code", 1355 | "execution_count": 28, 1356 | "metadata": {}, 1357 | "outputs": [ 1358 | { 1359 | "data": { 1360 | "text/plain": [ 1361 | "array (\n", 1362 | " 0 => '4124',\n", 1363 | " 1 => '4125',\n", 1364 | " 2 => '4126',\n", 1365 | " 3 => '4127',\n", 1366 | " 4 => '4128',\n", 1367 | " 5 => '4129',\n", 1368 | " 6 => '4130',\n", 1369 | ")\n" 1370 | ] 1371 | }, 1372 | "execution_count": 28, 1373 | "metadata": {}, 1374 | "output_type": "execute_result" 1375 | } 1376 | ], 1377 | "source": [ 1378 | "$recIds = [];\n", 1379 | "$fmdb->postalcode->startCommunication();\n", 1380 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 1\", \"f7\" => \"field 7 data\"]);\n", 1381 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 2\", \"f7\" => \"field 7 data\"]);\n", 1382 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 3\", \"f7\" => \"field 7 data\"]);\n", 1383 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 4\", \"f7\" => \"field 7 data\"]);\n", 1384 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 5\", \"f7\" => \"field 7 data\"]);\n", 1385 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 6\", \"f7\" => \"field 7 data\"]);\n", 1386 | "$recIds[] = $fmdb->postalcode->create([\"f3\" => \"field 3 data 7\", \"f7\" => \"field 7 data\"]);\n", 1387 | "$fmdb->postalcode->endCommunication();\n", 1388 | "echo var_export($recIds, true);" 1389 | ] 1390 | }, 1391 | { 1392 | "cell_type": "markdown", 1393 | "metadata": {}, 1394 | "source": [ 1395 | "## Parameter for Operations" 1396 | ] 1397 | }, 1398 | { 1399 | "cell_type": "markdown", 1400 | "metadata": {}, 1401 | "source": [ 1402 | "You can turn off to throw an exception in case of error. You have to handle errors with checking result error." 1403 | ] 1404 | }, 1405 | { 1406 | "cell_type": "code", 1407 | "execution_count": 29, 1408 | "metadata": {}, 1409 | "outputs": [ 1410 | { 1411 | "data": { 1412 | "text/plain": [ 1413 | "\u001b[36mnull\u001b[39m" 1414 | ] 1415 | }, 1416 | "execution_count": 29, 1417 | "metadata": {}, 1418 | "output_type": "execute_result" 1419 | } 1420 | ], 1421 | "source": [ 1422 | "$fmdb->setThrowException(false);" 1423 | ] 1424 | }, 1425 | { 1426 | "cell_type": "markdown", 1427 | "metadata": {}, 1428 | "source": [ 1429 | "If you set to throwing exceptions, you can describe the try-catch statement in your code for error handling." 1430 | ] 1431 | }, 1432 | { 1433 | "cell_type": "code", 1434 | "execution_count": 30, 1435 | "metadata": {}, 1436 | "outputs": [ 1437 | { 1438 | "data": { 1439 | "text/plain": [ 1440 | "[Exception]Field not_exist_field doesn't exist.\n" 1441 | ] 1442 | }, 1443 | "execution_count": 30, 1444 | "metadata": {}, 1445 | "output_type": "execute_result" 1446 | } 1447 | ], 1448 | "source": [ 1449 | "$fmdb->setThrowException(true);\n", 1450 | "$result = $fmdb->testtable->getRecord($recId);\n", 1451 | "try {\n", 1452 | " foreach ($result as $record) {\n", 1453 | " echo $record->not_exist_field;\n", 1454 | " }\n", 1455 | "} catch(Exception $ex) {\n", 1456 | " echo \"[Exception]{$ex->getMessage()}\";\n", 1457 | "}" 1458 | ] 1459 | }, 1460 | { 1461 | "cell_type": "markdown", 1462 | "metadata": {}, 1463 | "source": [ 1464 | "If you call with true, the debug mode is activated. Debug mode echos the contents of communication such as request and response." 1465 | ] 1466 | }, 1467 | { 1468 | "cell_type": "code", 1469 | "execution_count": 31, 1470 | "metadata": {}, 1471 | "outputs": [ 1472 | { 1473 | "data": { 1474 | "text/plain": [ 1475 | "\u001b[36mnull\u001b[39m" 1476 | ] 1477 | }, 1478 | "execution_count": 31, 1479 | "metadata": {}, 1480 | "output_type": "execute_result" 1481 | } 1482 | ], 1483 | "source": [ 1484 | "$fmdb->setDebug(false);" 1485 | ] 1486 | }, 1487 | { 1488 | "cell_type": "markdown", 1489 | "metadata": {}, 1490 | "source": [ 1491 | "If you call with true, the certificate from the server is going to verify. In case of self-signed one (usually default situation), you don't have to call this method." 1492 | ] 1493 | }, 1494 | { 1495 | "cell_type": "code", 1496 | "execution_count": 32, 1497 | "metadata": {}, 1498 | "outputs": [], 1499 | "source": [ 1500 | "//$fmdb->setCertValidating(true);" 1501 | ] 1502 | } 1503 | ], 1504 | "metadata": { 1505 | "kernelspec": { 1506 | "display_name": "PHP", 1507 | "language": "php", 1508 | "name": "jupyter-php" 1509 | }, 1510 | "language_info": { 1511 | "file_extension": ".php", 1512 | "mimetype": "text/x-php", 1513 | "name": "PHP", 1514 | "pygments_lexer": "PHP", 1515 | "version": "7.2.12" 1516 | } 1517 | }, 1518 | "nbformat": 4, 1519 | "nbformat_minor": 2 1520 | } 1521 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inter-mediator/fmdataapi", 3 | "version": "33", 4 | "time": "2024-09-16", 5 | "repositories": [ 6 | { 7 | "type": "git", 8 | "url": "https://github.com/msyk/FMDataAPI.git" 9 | } 10 | ], 11 | "prefer-stable": true, 12 | "require": { 13 | "php": ">=8.1", 14 | "ext-curl": "*", 15 | "ext-json": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "*", 19 | "phpstan/phpstan": "^2.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "INTERMediator\\FileMakerServer\\RESTAPI\\": "src/" 24 | }, 25 | "classmap": [ 26 | "test/FMDataAPIUnitTest.php", 27 | "test/TestProvider.php" 28 | ] 29 | }, 30 | "description": "FMDataAPI is the class library in PHP for accessing FileMaker database with FileMaker Data API.", 31 | "type": "library", 32 | "keywords": [ 33 | "INTER-Mediator", 34 | "FileMaker", 35 | "REST", 36 | "API" 37 | ], 38 | "homepage": "https://github.com/msyk/FMDataAPI", 39 | "license": "MIT", 40 | "authors": [ 41 | { 42 | "name": "Masayuki Nii (Auther)", 43 | "homepage": "http://msyk.net/" 44 | }, 45 | { 46 | "name": "Atsushi Matsuo (Contributor)" 47 | }, 48 | { 49 | "name": "darnel (Contributor)" 50 | }, 51 | { 52 | "name": "Craig Smith (Contributor)" 53 | }, 54 | { 55 | "name": "Bernhard Schulz (Contributor)" 56 | }, 57 | { 58 | "name": "montaniasystemab (Contributor)" 59 | }, 60 | { 61 | "name": "Rickard Andersson (Contributor)" 62 | }, 63 | { 64 | "name": "Julien @AnnoyingTechnology (Contributor)" 65 | }, 66 | { 67 | "name": "Tom Kuijer (Contributor)" 68 | }, 69 | { 70 | "name": "Thijs Meijer (Contributor)" 71 | }, 72 | { 73 | "name": "Patrick Janser (Contributor)" 74 | }, 75 | { 76 | "name": "Roger Engström (Contributor)" 77 | }, 78 | { 79 | "name": "Stathis Askaridis (Contributor)" 80 | } 81 | ], 82 | "support": { 83 | "community-jp": "https://www.facebook.com/groups/233378356708157/", 84 | "community-en": "https://www.facebook.com/groups/254446237922985/", 85 | "source": "https://github.com/msyk/FMDataAPI.git", 86 | "manual": "http://inter-mediator.info/FMDataAPI/namespaces/INTERMediator.FileMakerServer.RESTAPI.html" 87 | }, 88 | "scripts": { 89 | "test": [ 90 | "./vendor/bin/phpunit --bootstrap ./vendor/autoload.php --configuration ./test/phpunit.xml ./test/FMDataAPIUnitTest.php" 91 | ], 92 | "doc": [ 93 | "./vendor/bin/phpdoc -f ./src/FMDataAPI.php -t ../INTER-Mediator_Documents/FMDataAPI" 94 | ] 95 | }, 96 | "config": { 97 | "allow-plugins": { 98 | "symfony/flex": true 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: . 4 | platform: linux/amd64 5 | -------------------------------------------------------------------------------- /samples/FMDataAPI_Sample.php: -------------------------------------------------------------------------------- 1 | setThrowException(false); 38 | 39 | // If you call with true, the debug mode is activated. Debug mode echos the contents of communication 40 | // such as request and response. 41 | $fmdb->setDebug(true); 42 | 43 | // If you call with true, the certificate from the server is going to verify. 44 | // In the case of self-signed one (usually default situation), you don't have to call this method. 45 | //$fmdb->setCertValidating(true); 46 | 47 | // Metadata API is the new feature of FMS18. 48 | $pInfo = var_export($fmdb->getProductInfo(), true); 49 | echo htmlspecialchars("Product Info: {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 50 | $pInfo = var_export($fmdb->getDatabaseNames(), true); 51 | echo htmlspecialchars("Database Names: {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 52 | $pInfo = var_export($fmdb->getLayoutNames(), true); 53 | echo htmlspecialchars("Layout Names: {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 54 | $pInfo = var_export($fmdb->getScriptNames(), true); 55 | echo htmlspecialchars("Script Names: {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 56 | $result = $fmdb->person_layout->getMetadata(); 57 | $pInfo = var_export($result, true); 58 | echo htmlspecialchars("Layout Metadata: {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 59 | $result = $fmdb->person_layout->getMetadataOld(); 60 | $pInfo = var_export($result, true); 61 | echo htmlspecialchars("Layout Metadata (Old): {$pInfo}", ENT_QUOTES, "UTF-8") . "
"; 62 | 63 | // The FMDataAPI has the property as the same name of layout. This sample database has the 'person_layout' layout, 64 | // so '$fmdb->person_layout' refers FMLayout object fo the proxy of the layout. FMLayout class has the 'query' method 65 | // and returns FileMakerRelation class's object. The condition spread in parameter is the same as FileMaker's Find Record API. 66 | $result = $fmdb->person_layout->query(/*array(array("id" => ">1"))*/); 67 | 68 | // The 'httpStatus()' method returns the HTTP status code in the latest response. 69 | echo htmlspecialchars("HTTP Status: {$fmdb->httpStatus()}", ENT_QUOTES, "UTF-8") . "
"; 70 | 71 | // The following two methods return the error code and message of the latest API call which is submitted in query() method. 72 | // You can check API calling succeed or fail if error code is or isn't 0 every after API calling methods. 73 | echo htmlspecialchars("Error Code: {$fmdb->errorCode()}", ENT_QUOTES, "UTF-8") . "
"; 74 | echo htmlspecialchars("Error Message: {$fmdb->errorMessage()}", ENT_QUOTES, "UTF-8") . "
"; 75 | 76 | // If the query is succeeded, the following information can be detected. 77 | echo htmlspecialchars("Target Table: {$fmdb->getTargetTable()}", ENT_QUOTES, "UTF-8") . "
"; 78 | echo htmlspecialchars("Total Count: {$fmdb->getTotalCount()}", ENT_QUOTES, "UTF-8") . "
"; 79 | echo htmlspecialchars("Found Count: {$fmdb->getFoundCount()}", ENT_QUOTES, "UTF-8") . "
"; 80 | echo htmlspecialchars("Returned Count: {$fmdb->getReturnedCount()}", ENT_QUOTES, "UTF-8") . "
"; 81 | 82 | // The FileMakerRelation class implements the Iterator interface, and it can repeat with 'foreach.' 83 | // The $record also refers to a FileMakerRelation object, but it is for a single record. 84 | // This layout has fields as like 'id', 'name', 'mail' and so on, and the field name can be handled 85 | // as a property name of the record referring to $record. 86 | if (!is_null($result)) { 87 | // If the query is succeeded, the following information can be detected. 88 | echo htmlspecialchars("Target Table: {$result->getTargetTable()}", ENT_QUOTES, "UTF-8") . "
"; 89 | echo htmlspecialchars("Total Count: {$result->getTotalCount()}", ENT_QUOTES, "UTF-8") . "
"; 90 | echo htmlspecialchars("Found Count: {$result->getFoundCount()}", ENT_QUOTES, "UTF-8") . "
"; 91 | echo htmlspecialchars("Returned Count: {$result->getReturnedCount()}", ENT_QUOTES, "UTF-8") . "
"; 92 | foreach ($result as $record) { 93 | echo htmlspecialchars("id: {$record->id},", ENT_QUOTES, "UTF-8"); 94 | echo htmlspecialchars("name: {$record->name},", ENT_QUOTES, "UTF-8"); 95 | echo htmlspecialchars("mail: {$record->mail}", ENT_QUOTES, "UTF-8") . "
"; 96 | // If you named field name as not variable friendly, you can use field('field_name') method or 97 | // set the name to any variable such as $fname = 'field_name'; echo $record->$fname;. 98 | 99 | // In the case of a related field but outside portal, the field method is available as below: 100 | // echo $record->field("summary", "contact_to"); 101 | 102 | // A portal name property returns records of portal as FileMakerRelation object. 103 | $contacts = $record->Contact; 104 | 105 | // If the query is succeeded, the following information can be detected. 106 | echo htmlspecialchars("Target Table: {$contacts->getTargetTable()}", ENT_QUOTES, "UTF-8") . "
"; 107 | echo htmlspecialchars("Total Count: {$contacts->getTotalCount()}", ENT_QUOTES, "UTF-8") . "
"; 108 | echo htmlspecialchars("Found Count: {$contacts->getFoundCount()}", ENT_QUOTES, "UTF-8") . "
"; 109 | echo htmlspecialchars("Returned Count: {$contacts->getReturnedCount()}", ENT_QUOTES, "UTF-8") . "
"; 110 | 111 | // You can repeat with foreach for the portal records. 112 | foreach ($contacts as $item) { 113 | // Technically portal field has to be refered as "contact_to::id" but it can be an indentifier in PHP. 114 | // In this case, you can call field method as like 'field("summary", "contact_to").' 115 | // If the field belongs to the table occurrence for the portal, you can refer the field as like '$item->id.' 116 | // If the field belongs to another table occurrence, you have to call the 'field()' method. 117 | echo htmlspecialchars("[PORTAL(contact_to)] id: {$item->field("id", "contact_to")},", ENT_QUOTES, "UTF-8"); 118 | echo htmlspecialchars("summary: {$item->field("summary", "contact_to")}", ENT_QUOTES, "UTF-8") . "
"; 119 | // If the object name of the portal is blank, it can be referred as the table occurrence name. 120 | // If the object name is specified, you have to access with the object name, and it means you have to 121 | // call 'field()' method to get the value. 122 | } 123 | echo "
"; 124 | } 125 | 126 | 127 | echo "

toArray() results

"; 128 | echo "

[query_result]->toArray()

"; 129 | var_export($result->toArray()); 130 | 131 | foreach ($result as $record) { 132 | echo "
"; 133 | echo "

[each_record]->toArray()

"; 134 | var_export($record->toArray()); 135 | foreach ($result->getPortalNames() as $portalName) { 136 | echo "

[portal]->toArray()

"; 137 | var_export($record->$portalName->toArray()); 138 | foreach ($record->$portalName as $portalRecord) { 139 | echo "

[each_portal_record]->toArray()

"; 140 | var_export($portalRecord->toArray()); 141 | } 142 | } 143 | } 144 | 145 | // Move to the pointer to the first record. 146 | $result->rewind(); 147 | 148 | // The FileMakerRelation object from 'query()' method can be accessed as like the 'cursor' style repeating. 149 | // The 'count()' method returns the number of records in response. The variable $result refers current 150 | // record, and you can get the field value with the propaty having the same field name. 151 | // The portal can be done with same way. The 'next()' method steps forward the pointer of the current record. 152 | for ($i = 0; $i < $result->count(); $i++) { 153 | echo htmlspecialchars("id: {$result->id},", ENT_QUOTES, "UTF-8"); 154 | echo htmlspecialchars("name: {$result->name},", ENT_QUOTES, "UTF-8"); 155 | echo htmlspecialchars("mail: {$result->mail}", ENT_QUOTES, "UTF-8") . "
"; 156 | $contacts = $result->Contact; 157 | 158 | for ($j = 0; $j < $contacts->count(); $j++) { 159 | echo htmlspecialchars("[PORTAL(contact_to)] id: {$contacts->field("id", "contact_to")},", ENT_QUOTES, "UTF-8"); 160 | echo htmlspecialchars("summary: {$contacts->field("summary", "contact_to")}", ENT_QUOTES, "UTF-8") . "
"; 161 | $contacts->next(); 162 | } 163 | $result->next(); 164 | } 165 | } 166 | // The 'create()' method creates a record with values in parameter. 167 | // The associated array of the parameter has to be a series of field name key and its value. 168 | $recId = $fmdb->postalcode->create(array("f3" => "field 3 data", "f7" => "field 7 data")); 169 | 170 | // The 'getRecord()' method query the record with the recordId of the parameter. 171 | // It returns the FileMakerRelation object, and you can handle it with the return value from 'query()' method. 172 | $result = $fmdb->postalcode->getRecord($recId); 173 | if (!is_null($result)) { 174 | foreach ($result as $record) { 175 | echo htmlspecialchars("f3: {$record->f3},", ENT_QUOTES, "UTF-8"); 176 | echo htmlspecialchars("f7: {$record->f7},", ENT_QUOTES, "UTF-8"); 177 | echo htmlspecialchars("f8: {$record->f8}", ENT_QUOTES, "UTF-8") . "
"; 178 | echo "
"; 179 | } 180 | } 181 | 182 | // The 'update()' method modifies fields in a record. You have to set parameters as the recordId of target 183 | // record and associated array to specify the modified data. 184 | $fmdb->postalcode->update($recId, array("f3" => "field 3 modifed", "f8" => "field 8 update")); 185 | $result = $fmdb->postalcode->getRecord($recId); 186 | if (!is_null($result)) { 187 | foreach ($result as $record) { 188 | echo htmlspecialchars("f3: {$record->f3},", ENT_QUOTES, "UTF-8"); 189 | echo htmlspecialchars("f7: {$record->f7},", ENT_QUOTES, "UTF-8"); 190 | echo htmlspecialchars("f8: {$record->f8}", ENT_QUOTES, "UTF-8") . "
"; 191 | echo "
"; 192 | } 193 | } 194 | // The 'delete()' method deletes the record specified by the parameter. 195 | $fmdb->postalcode->delete($recId); 196 | 197 | // Call script 198 | $result = $fmdb->person_layout->query(null, null, -1, 1, null, ["script" => "TestScript", "script.param" => "ok"]); 199 | if (!is_null($result)) { 200 | echo htmlspecialchars("Script Error: {$fmdb->person_layout->getScriptError()}", ENT_QUOTES, "UTF-8") . "
"; 201 | echo htmlspecialchars("Script Result: {$fmdb->person_layout->getScriptResult()}", ENT_QUOTES, "UTF-8") . "
"; 202 | } 203 | $result = $fmdb->person_layout->query(null, null, -1, 1, null, ["script.prerequest" => "TestScript", "script.prerequest.param" => "ok"]); 204 | if (!is_null($result)) { 205 | echo htmlspecialchars("Script Error: {$fmdb->person_layout->getScriptErrorPrerequest()}", ENT_QUOTES, "UTF-8") . "
"; 206 | echo htmlspecialchars("Script Result: {$fmdb->person_layout->getScriptResultPrerequest()}", ENT_QUOTES, "UTF-8") . "
"; 207 | } 208 | $result = $fmdb->person_layout->query(null, null, -1, 1, null, ["script" => "TestScript", "script.param" => "not"]); 209 | if (!is_null($result)) { 210 | echo htmlspecialchars("Script Error: {$fmdb->person_layout->getScriptError()}", ENT_QUOTES, "UTF-8") . "
"; 211 | echo htmlspecialchars("Script Result: {$fmdb->person_layout->getScriptResult()}", ENT_QUOTES, "UTF-8") . "
"; 212 | } 213 | $result = $fmdb->person_layout->query(null, null, -1, 1); 214 | if (!is_null($result)) { 215 | echo htmlspecialchars("Script Error: {$fmdb->person_layout->getScriptError()}", ENT_QUOTES, "UTF-8") . "
"; 216 | echo htmlspecialchars("Script Result: {$fmdb->person_layout->getScriptResult()}", ENT_QUOTES, "UTF-8") . "
"; 217 | } 218 | 219 | // A new record is created in "testtable" table. 220 | $recId = $fmdb->testtable->create(); 221 | echo "RecId = {$recId}"; 222 | // The "testtable" table has a container filed "vc1". One image file is going to be uploaded to it. 223 | // The file path, record id and field name are required. 224 | $fmdb->testtable->uploadFile("cat.jpg", $recId, "vc1"); 225 | // What kind of data does the container field which inserted an image return? 226 | // For example, the returned value was like this: 227 | // https://localhost/Streaming_SSL/MainDB/6A4A253F7CE33465DCDFBFF0704B34C0993D54AD85702396920E85249BD0271A.jpg?RCType=EmbeddedRCFileProcessor 228 | // This url can get the content of the container field, and it means you can download with file_put_content() function and so on. 229 | $result = $fmdb->testtable->getRecord($recId); 230 | echo htmlspecialchars("Target Table(getRecord): {$result->getTargetTable()}", ENT_QUOTES, "UTF-8") . "
"; 231 | echo htmlspecialchars("Total Count(getRecord): {$result->getTotalCount()}", ENT_QUOTES, "UTF-8") . "
"; 232 | echo htmlspecialchars("Found Count(getRecord): {$result->getFoundCount()}", ENT_QUOTES, "UTF-8") . "
"; 233 | echo htmlspecialchars("Returned Count(getRecord): {$result->getReturnedCount()}", ENT_QUOTES, "UTF-8") . "
"; 234 | 235 | if (!is_null($result)) { 236 | foreach ($result as $record) { 237 | echo htmlspecialchars("vc1: {$record->vc1}", ENT_QUOTES, "UTF-8") . "
"; 238 | echo "

"; 239 | } 240 | } 241 | 242 | // If you call the 'startCommunication()' method, you can describe a series of database operation 243 | // calls. This means the authentication is going to be done at the 'startCommunication()' method, 244 | // and the token is going to be shared with the following statements. The 'endCommunication()' calls 245 | // logout REST API call and invalidates the shared token. 246 | $recIds = array(); 247 | $fmdb->postalcode->startCommunication(); 248 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 1", "f7" => "field 7 data")); 249 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 2", "f7" => "field 7 data")); 250 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 3", "f7" => "field 7 data")); 251 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 4", "f7" => "field 7 data")); 252 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 5", "f7" => "field 7 data")); 253 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 6", "f7" => "field 7 data")); 254 | $recIds[] = $fmdb->postalcode->create(array("f3" => "field 3 data 7", "f7" => "field 7 data")); 255 | $fmdb->postalcode->endCommunication(); 256 | var_export($recIds); 257 | echo "
"; 258 | 259 | // The 'query()' method can have several parameters. The portal specification has to be an array 260 | // with the object name of the portal, not the table occurrence name. 261 | $portal = array("Contact"); 262 | $result = $fmdb->person_layout->query(array(array("id" => "1")), null, 1, -1, $portal); 263 | if (!is_null($result)) { 264 | foreach ($result as $record) { 265 | $recordId = $record->getRecordId(); 266 | $partialResult = $fmdb->person_layout->getRecord($recordId, $portal); 267 | var_export($partialResult); 268 | echo "
"; 269 | } 270 | } 271 | // The 'query()' method can have several parameters. The second parameter is for sorting. 272 | $portal = array("Contact"); 273 | $result = $fmdb->person_layout->query(array(array("id" => "1...")), array(array("id", "descend")), 1, -1, $portal); 274 | if (!is_null($result)) { 275 | foreach ($result as $record) { 276 | $recordId = $record->getRecordId(); 277 | $partialResult = $fmdb->person_layout->getRecord($recordId, $portal); 278 | var_export($partialResult); 279 | echo "
"; 280 | } 281 | } 282 | // The 'query()' method can have several parameters. 283 | // The forth parameter is limit record number to query, and the third is offset. 284 | $result = $fmdb->person_layout->query(null, null, 2, 2); 285 | if (!is_null($result)) { 286 | foreach ($result as $record) { 287 | $recordId = $record->getRecordId(); 288 | $partialResult = $fmdb->person_layout->getRecord($recordId, $portal); 289 | var_export($partialResult); 290 | echo "
"; 291 | } 292 | } 293 | 294 | // The getFirstRecord method returns FileMakerRelation class object. 295 | $result = $fmdb->person_layout->query(); 296 | $first = $result->getFirstRecord(); 297 | echo "id field of the first record: {$first->field('id')}
"; 298 | $portals = $first->getPortalNames(); 299 | echo "getPortalNames of the first record: " . var_export($portals, true) . "
"; 300 | $contacts = $first->Contact; 301 | echo "[PORTAL(contact_to)] id: {$contacts->field("id", "contact_to")}
"; 302 | 303 | } catch (Exception $e) { 304 | echo '

例外発生

', htmlspecialchars($e->getMessage(), ENT_QUOTES, "UTF-8"), "
"; 305 | } 306 | -------------------------------------------------------------------------------- /samples/HISTORY.md: -------------------------------------------------------------------------------- 1 | # FMDataAPI's previous history 2 | 3 | - April 2017: Start to create these classes and codes. 4 | - 2017-05-05: README.md added. 5 | - 2017-05-26: [Ver.2] Support the "localserver" as host name. 6 | - 2017-05-31: [Ver.3] The query() method of FileMakerLayout class fixed. 7 | 'Offset' and 'range' parameters could not set as an integer value. 8 | - 2017-11-06: [Ver.4] The getFieldNames() and getPortalNames() methods added. 9 | - 2018-02-03: [Ver.5] Bug fix of sorting parameters in query method. 10 | - 2018-02-18: [Ver.6] Bug fix of creating record with no default value. 11 | - 2018-03-25: [Ver.7] getSessionToken method added. OAuth handling implemented but not well debugged. 12 | - 2018-05-09: The Version 7 is the last version that supports FileMaker 16-based Data API. 13 | - 2018-05-15: [Ver.8] Update for FileMaker 17. FileMaker Data API v1 is supported from this version. 14 | The preview version of FileMaker Data API doesn't support anymore. 15 | - 2018-05-27: [Ver.9] composer.json is added, and can install "inter-mediator/fmdataapi." 16 | FMDataAPITrial directory deleted because it's already discontinued api. 17 | Add the "samples" directory and move sample files into it. 18 | - 2018-06-22: [Ver.10] Added the getContainerData method (Thanks to base64bits!), 19 | bug fix (Thanks to phpsa!). 20 | - 2018-07-22: [Ver.11] Global field methods bug fixed and were available in FMDataAPI class (Tanks to Mr.Matsuo). 21 | The script errors and results can get from methods in FMLayout class. 22 | - 2018-07-29: [Ver.12] Bug fix for UUID Supporting (Thanks to Mr.Matsuo). 23 | Unit tests implemented but now for limited methods, als integrating Travis CI. 24 | - 2018-11-13: [Ver.13] 25 | Added getDebugInfo method (Thanks to Mr.Matsuo), 26 | modified and fixed the getFieldNames method (Thanks to phpsa), 27 | fixed handling portal object name (Thanks to Mr.Matsuo) 28 | fixed the getModId method (Thanks to Flexboom) 29 | - 2018-11-17: [Ver.15] 30 | Jupyter Notebook style sample and results. 31 | - 2019-05-19: [Ver.16] 32 | This is the final version for FileMaker 17 platform, and bug fix (Thanks to darnel) 33 | - 2019-05-20: [Ver.17] 34 | Support the FileMaker 18 platform. 35 | Add getMetadataOld() and getMetadata() to FileMakerLayout class. 36 | Add getProductInfo(), getDatabaseNames(), getLayoutNames() and getScriptNames() to FMDataAPI class. 37 | - 2019-05-27: [Ver.18] 38 | Add getTargetTable(), getTotalCount(), getFoundCount(), getReturnedCount() to FileMakerRelation class. 39 | Add getTargetTable(), getTotalCount(), getFoundCount(), getReturnedCount() to FMDataAPI class. 40 | - 2019-09-12: [Ver.19] 41 | Add the duplicate() method to the FileMakerLayout class. Thanks to schube. 42 | - 2019-09-16: [Ver.20] 43 | The default values of limit and range parameters changed to 0 and both just applied for over 0 values. Thanks to schube. 44 | - 2020-08-23: [Ver.21] 45 | Bug fix about the field referencing of a related field without any portals. Thanks to frankeg. 46 | Check on the FileMaker Server 19. 47 | 48 | (History of recent date is [here](../README.md)) 49 | 50 | ## API Differences between ver.8 and 7. 51 | ### FMDataAPI class 52 | The setAPIVersion method added. This is for a future update of FileMaker Data API. 53 | As far as FMDataAPI Ver. 8 goes, This isn't required. 54 | - public function __construct($solution, $user, $password, $host = NULL, $port = NULL, $protocol = NULL, [New]$fmDataSource = null) 55 | - [New]public function setAPIVersion($vNum) 56 | 57 | ### FileMakerRelation class 58 | The following methods added to script parameters. See the query method's document for specifying it. 59 | The methods added to portal parameter. 60 | 61 | - public function query($condition = NULL, $sort = NULL, $offset = -1, $range = -1, $portal = null, [New]$script = null) 62 | - public function getRecord($recordId, $portal = null, [New]$script = null) 63 | - public function create($data = null, [New]$portal = null, [New]$script = null) 64 | - public function delete($recordId, [New]$script = null) 65 | - public function update($recordId, $data, $modId = -1, [New]$portal = null, [New]$script = null) 66 | - [New]public function uploadFile($filePath, $recordId, $containerFieldName, $containerFieldRepetition = null, $fileName = null) 67 | -------------------------------------------------------------------------------- /samples/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msyk/FMDataAPI/71032a349d629313d0138b276cd64bba716bcf25/samples/cat.jpg -------------------------------------------------------------------------------- /src/FMDataAPI.php: -------------------------------------------------------------------------------- 1 | 19 | * @copyright 2017-2024 Masayuki Nii 20 | * (Claris FileMaker is registered trademarks of Claris International Inc. in the U.S. and other countries.) 21 | */ 22 | class FMDataAPI 23 | { 24 | /* Document generating: 25 | * - Install PHP Documentor, and enter command below. 26 | * php ../phpDocumentor.phar run -f ./src/FMDataAPI.php -f ./src/Supporting/CommunicationProvider.php -f ./src/Supporting/FileMakerLayout.php -f ./src/Supporting/FileMakerRelation.php -t ../INTER-Mediator_Documents/FMDataAPI 27 | */ 28 | 29 | /** 30 | * @var FileMakerLayout[] Keeping the FileMakerLayout object for each layout. 31 | * @ignore 32 | */ 33 | private array $layoutTable = []; 34 | 35 | /** 36 | * @var null|CommunicationProvider Keeping the CommunicationProvider object. 37 | * @ignore 38 | */ 39 | private CommunicationProvider|null $provider; 40 | 41 | /** 42 | * FMDataAPI constructor. If you want to activate OAuth authentication, $user and $password are set as 43 | * oAuthRequestId and oAuthIdentifier. Moreover, call useOAuth method before accessing layouts. 44 | * @param string $solution The database file name which is just hosting. 45 | * Every database must have the accessing privilege 'fmrest' including external data sources. 46 | * @param string $user The fmrest privilege accessible user to the database. 47 | * If you’re going to call useOAuth method, you have to specify the data for X-FM-Data-OAuth-Request-Id. 48 | * @param string|null $password The password of the above user. 49 | * This can be null for testing purpose only. Null data is going to replace the string "password". 50 | * This prevents to be detected as a security issue. 51 | * On the real solutions, you have to set a valid password string. 52 | * If you’re going to call useOAuth method, you have to specify the data for X-FM-Data-OAuth-Identifier. 53 | * @param string|null $host FileMaker Server's host name or IP address. If omitted, 'localhost' is chosen. 54 | * The value "localserver" tries to connect directory 127.0.0.1, and you don't have to set $port and $protocol. 55 | * @param int|null $port FileMaker Server's port number. If omitted, 443 is chosen. 56 | * @param string|null $protocol FileMaker Server's protocol name. If omitted, 'https' is chosen. 57 | * @param array|null $fmDataSource Authentication information for external data sources. 58 | * Ex. [{"database"=>"", "username"=>"", "password"=>""}]. 59 | * If you use OAuth, "oAuthRequestId" and "oAuthIdentifier" keys have to be specified. 60 | * @param boolean $isUnitTest If it's set to true, the communication provider just works locally. 61 | */ 62 | public function __construct(string $solution, 63 | string $user, 64 | string|null $password, 65 | string|null $host = null, 66 | int|null $port = null, 67 | string|null $protocol = null, 68 | array|null $fmDataSource = null, 69 | bool $isUnitTest = false) 70 | { 71 | if (is_null($password)) { 72 | $password = "password"; // For testing purpose. 73 | } 74 | if (!$isUnitTest) { 75 | $this->provider = new Supporting\CommunicationProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource); 76 | } else { 77 | $this->provider = new Supporting\TestProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource); 78 | } 79 | } 80 | 81 | /** 82 | * Can't set the value to the undefined name. 83 | * @param string $key The property name 84 | * @param mixed $value The value to set 85 | * @throws Exception 86 | * @ignore 87 | */ 88 | public function __set(string $key, 89 | mixed $value): void 90 | { 91 | throw new Exception("The $key property is read-only, and can't set any value."); 92 | } 93 | 94 | /** 95 | * Handle the undefined name as the layout name. 96 | * @param string $key The property name 97 | * @return FileMakerLayout FileMakerLayout object 98 | * @ignore 99 | */ 100 | public function __get(string $key): FileMakerLayout 101 | { 102 | return $this->layout($key); 103 | } 104 | 105 | /** 106 | * Refers the FileMakerLayout object as the proxy of the layout. 107 | * If the layout doesn't exist, no error arises here. Any errors might arise on methods of FileMakerLayout class. 108 | * @param string $layout_name Layout name. 109 | * @return FileMakerLayout object which is proxy of FileMaker's layout. 110 | */ 111 | public function layout(string $layout_name): FileMakerLayout 112 | { 113 | if (!isset($this->layoutTable[$layout_name])) { 114 | $this->layoutTable[$layout_name] = new Supporting\FileMakerLayout($this->provider, $layout_name); 115 | } 116 | return $this->layoutTable[$layout_name]; 117 | } 118 | 119 | /** 120 | * Set the debug mode or not. The debug mode isn't in default. 121 | * @param bool $value set the debug mode if the value is true. 122 | */ 123 | public function setDebug(bool $value): void 124 | { 125 | $this->provider->isDebug = $value; 126 | } 127 | 128 | /** 129 | * Set the cURL communication timeout in seconds 130 | * @param int $timeout 131 | */ 132 | public function setTimeout(int $timeout): void 133 | { 134 | $this->provider->timeout = $timeout; 135 | } 136 | 137 | /** 138 | * On the authentication session, username and password are handled as OAuth parameters. 139 | */ 140 | public function useOAuth(): void 141 | { 142 | $this->provider->useOAuth = true; 143 | } 144 | 145 | /** 146 | * FileMaker Data API's version is going to be set. If you don't call, the "vLatest" is specified. 147 | * As far as FileMaker 18 supports just "v1", no one has to call this method. 148 | * @param int $vNum FileMaker Data API's version number. 149 | */ 150 | public function setAPIVersion(int $vNum): void 151 | { 152 | $this->provider->vNum = $vNum; 153 | } 154 | 155 | /** 156 | * Set to verify the server certificate. The default is to handle as self-signed certificate and doesn't verify. 157 | * @param bool $value Turn on to verify the certificate if the value is true. 158 | */ 159 | public function setCertValidating(bool $value): void 160 | { 161 | $this->provider->isCertValidating = $value; 162 | } 163 | 164 | /** 165 | * Set to true if the return value of the field() method uses the htmlspecialchars function. 166 | * The default value is FALSE. 167 | * The nostalgic FileMaker API for PHP was returning the htmlspecialchars value of the field. 168 | * If we want to get the row field data, we had to call the getFieldUnencoded method. 169 | * If this property is set to true, 170 | * FileMakerRelation class's field method (including describing field name directly) returns the value processed 171 | * with the htmlspecialchars. 172 | * This means kind of compatible mode to FileMaker API for PHP. 173 | * This feature works whole the FMDataAPI library. 174 | * @param bool $value Turn on to verify the certificate if the value is true. 175 | */ 176 | public function setFieldHTMLEncoding(bool $value): void 177 | { 178 | $this->provider->fieldHTMLEncoding = $value; 179 | } 180 | 181 | /** 182 | * Detect the return value of the field() method uses htmlspecialchars function or not. 183 | * @return bool The result. 184 | */ 185 | public function getFieldHTMLEncoding(): bool 186 | { 187 | return $this->provider->fieldHTMLEncoding; 188 | } 189 | 190 | /** 191 | * Set session token 192 | * @param string $value The session token. 193 | */ 194 | public function setSessionToken(string $value): void 195 | { 196 | $this->provider->accessToken = $value; 197 | } 198 | 199 | /** 200 | * The session token earned after authentication. 201 | * @return string The session token. 202 | */ 203 | public function getSessionToken(): string 204 | { 205 | return $this->provider->accessToken; 206 | } 207 | 208 | /** 209 | * The error number of curl, i.e., kind of communication error code. 210 | * @return int The error number of curl. 211 | */ 212 | public function curlErrorCode(): int 213 | { 214 | return $this->provider->curlErrorNumber; 215 | } 216 | 217 | /** 218 | * The error message of curl, text representation of code. 219 | * @return string|null The error message of curl. 220 | */ 221 | public function curlErrorMessage(): null|string 222 | { 223 | return $this->provider->curlError; 224 | } 225 | 226 | /** 227 | * The HTTP status code of the latest response from the REST API. 228 | * @return int|null The HTTP status code. 229 | */ 230 | public function httpStatus(): int|null 231 | { 232 | return $this->provider->httpStatus; 233 | } 234 | 235 | /** 236 | * The error code of the latest response from the REST API. 237 | * Code 0 means no error, and -1 means error information wasn't returned. 238 | * This error code is associated with FileMaker's error code. 239 | * @return int The error code. 240 | */ 241 | public function errorCode(): int 242 | { 243 | return $this->provider->errorCode; 244 | } 245 | 246 | /** 247 | * The error message of the latest response from the REST API. 248 | * This error message is associated with FileMaker's error code. 249 | * @return string|null The error message. 250 | */ 251 | public function errorMessage(): string|null 252 | { 253 | return $this->provider->errorMessage; 254 | } 255 | 256 | /** 257 | * Set to prevent throwing an exception in case of error. 258 | * The default is true, so an exception is going to throw in error. 259 | * @param bool $value Turn off to throw an exception in case of error if the value is false. 260 | */ 261 | public function setThrowException(bool $value): void 262 | { 263 | $this->provider->throwExceptionInError = $value; 264 | } 265 | 266 | /** 267 | * Start a transaction which is a serial calling of multiple database operations before the single authentication. 268 | * Usually most methods login and logout before/after the database operation, and so a little bit of time is going to 269 | * take. 270 | * The startCommunication() login and endCommunication() logout, and methods between them don't log in/out, and 271 | * it can expect faster operations. 272 | * @throws Exception 273 | */ 274 | public function startCommunication(): void 275 | { 276 | try { 277 | if ($this->provider->login()) { 278 | $this->provider->keepAuth = true; 279 | } 280 | } catch (Exception $e) { 281 | $this->provider->keepAuth = false; 282 | throw $e; 283 | } 284 | } 285 | 286 | /** 287 | * Finish a transaction which is a serial calling of any database operations, and logout. 288 | * @throws Exception 289 | */ 290 | public function endCommunication(): void 291 | { 292 | $this->provider->keepAuth = false; 293 | $this->provider->logout(); 294 | } 295 | 296 | /** 297 | * Set the value to the global field. 298 | * @param array $fields Associated array contains the global field names (Field names must be Fully Qualified) and its values. 299 | * Keys are global field names and values is these values. 300 | * @throws Exception In case of any error, an exception arises. 301 | */ 302 | public function setGlobalField(array $fields): void 303 | { 304 | if ($this->provider->login()) { 305 | $headers = ["Content-Type" => "application/json"]; 306 | $params = ["globals" => null]; 307 | $request = ["globalFields" => $fields]; 308 | $this->provider->callRestAPI($params, true, "PATCH", $request, $headers); // Throw Exception 309 | $this->provider->storeToProperties(); 310 | $this->provider->logout(); 311 | } 312 | } 313 | 314 | /** 315 | * Get the product information, such as the version, etc. This isn't required to authenticate. 316 | * @return null|object The information of this FileMaker product. Ex.: 317 | * {'name' => 'FileMaker Data API Engine', 'buildDate' => '03/27/2019', 'version' => '18.0.1.109', 318 | * 'dateFormat' => 'MM/dd/yyyy', 'timeFormat' => 'HH:mm:ss', 'timeStampFormat' => 'MM/dd/yyyy HH:mm:ss'}. 319 | * @throws Exception In case of any error, an exception arises. 320 | */ 321 | public function getProductInfo(): null|object 322 | { 323 | return $this->provider->getProductInfo(); 324 | } 325 | 326 | /** 327 | * Get the information about hosting database. It includes the target database and others in FileMaker Server. 328 | * This is required to authenticate. 329 | * @return null|array The information of hosting databases. Every element is an object and just having 'name' 330 | * property.Ex.: [{"name": "TestDB"},{"name": "sample_db"}] 331 | * @throws Exception In case of any error, an exception arises. 332 | */ 333 | public function getDatabaseNames(): null|array 334 | { 335 | return $this->provider->getDatabaseNames(); 336 | } 337 | 338 | /** 339 | * Get the list of layout name in a database. 340 | * @return null|array The information of layouts in the target database. Every element is an object and just having 'name' 341 | * property. 342 | * Ex.: [{"name": "person_layout"},{"name": "contact_to"},{"name": "history_to"}...] 343 | * @throws Exception In case of any error, an exception arises. 344 | */ 345 | public function getLayoutNames(): null|array 346 | { 347 | return $this->provider->getLayoutNames(); 348 | } 349 | 350 | /** 351 | * Get the list of script name in database. 352 | * @return null|array The information of scripts in the target database. Every element is an object and having 'name' property. 353 | * The 'isFolder' property is true if it's a folder item, and it has the 'folderScriptNames' property and includes 354 | * an object with the same structure. 355 | * Ex.: [{"name": "TestScript1","isFolder": false},{"name": "TestScript2","isFolder": false},{"name": "Maintenance", 356 | * "isFolder": true, "folderScriptNames": [{"name": "DataImport","isFolder": false}]}] 357 | * @throws Exception In case of any error, an exception arises. 358 | */ 359 | public function getScriptNames(): null|array 360 | { 361 | return $this->provider->getScriptNames(); 362 | } 363 | 364 | /** 365 | * Get the table occurrence name of just a previous query. 366 | * Usually this method returns the information of the FileMakerRelation class. 367 | * @return null|string The table name. 368 | * @see FileMakerRelation::getTargetTable() 369 | */ 370 | public function getTargetTable(): null|string 371 | { 372 | return $this->provider->targetTable; 373 | } 374 | 375 | /** 376 | * Get the total record count of just a previous query. 377 | * Usually this method returns the information of the FileMakerRelation class. 378 | * @return null|int The total record count. 379 | * @see FileMakerRelation::getTotalCount() 380 | */ 381 | public function getTotalCount(): null|int 382 | { 383 | return $this->provider->totalCount; 384 | } 385 | 386 | /** 387 | * Get the founded record count of just a previous query. 388 | * Usually this method returns the information of the FileMakerRelation class. 389 | * @return null|int The founded record count. 390 | * @see FileMakerRelation::getFoundCount(): null|int 391 | */ 392 | public function getFoundCount(): null|int 393 | { 394 | return $this->provider->foundCount; 395 | } 396 | 397 | /** 398 | * Get the returned record count of just a previous query. 399 | * Usually this method returns the information of the FileMakerRelation class. 400 | * @return null|int The returned record count. 401 | * @see FileMakerRelation::getReturnedCount() 402 | */ 403 | public function getReturnedCount(): null|int 404 | { 405 | return $this->provider->returnedCount; 406 | } 407 | 408 | /** 409 | * Return the base64 encoded data in container field with streaming url. 410 | * @param string $url The container data URL. 411 | * @return string The base64 encoded data in container field. 412 | * @throws Exception The exception from the accessToContainer method. 413 | */ 414 | public function getContainerData(string $url): string 415 | { 416 | return $this->provider->accessToContainer($url); 417 | } 418 | 419 | } 420 | -------------------------------------------------------------------------------- /src/Supporting/CommunicationProvider.php: -------------------------------------------------------------------------------- 1 | 15 | * @copyright 2017-2024 Masayuki Nii (Claris FileMaker is registered trademarks of Claris International Inc. in the U.S. and other countries.) 16 | */ 17 | class CommunicationProvider 18 | { 19 | /** 20 | * @var int 21 | * @ignore 22 | */ 23 | public int $vNum = -1; 24 | /** 25 | * @var null|string 26 | * @ignore 27 | */ 28 | private string|null $host = "127.0.0.1"; 29 | /** 30 | * @var string 31 | * @ignore 32 | */ 33 | private string $user; 34 | /** 35 | * @var string 36 | * @ignore 37 | */ 38 | private string $password; 39 | /** 40 | * @var string|null 41 | * @ignore 42 | */ 43 | private string|null $solution; 44 | /** 45 | * @var null|string 46 | * @ignore 47 | */ 48 | private string|null $protocol = 'https'; 49 | /** 50 | * @var int|null 51 | * @ignore 52 | */ 53 | private int|null $port = 443; 54 | 55 | /** 56 | * @var string|null 57 | * @ignore 58 | */ 59 | public string|null $accessToken = null; 60 | /** 61 | * @var string 62 | * @ignore 63 | */ 64 | protected string $method; 65 | /** 66 | * @var string 67 | * @ignore 68 | */ 69 | public string $url; 70 | /** 71 | * @var array 72 | * @ignore 73 | */ 74 | protected array $requestHeader; 75 | /** 76 | * @var null|array|string 77 | * @ignore 78 | */ 79 | public null|array|string $requestBody = ""; 80 | /** 81 | * @var int 82 | * @ignore 83 | */ 84 | public int $curlErrorNumber = 0; 85 | /** 86 | * @var string 87 | * @ignore 88 | */ 89 | public string $curlError = ""; 90 | /** 91 | * @var null|array 92 | * @ignore 93 | */ 94 | protected null|array $curlInfo; 95 | /** 96 | * @var string 97 | * @ignore 98 | */ 99 | private string $responseHeader; 100 | /** 101 | * @var bool 102 | * @ignore 103 | */ 104 | private bool $isLocalServer = false; 105 | /** 106 | * @var null|string 107 | * @ignore 108 | */ 109 | public null|string $targetTable = ''; 110 | /** 111 | * @var null|int 112 | * @ignore 113 | */ 114 | public null|int $totalCount = null; 115 | /** 116 | * @var null|int 117 | * @ignore 118 | */ 119 | public null|int $foundCount = null; 120 | /** 121 | * @var null|int 122 | * @ignore 123 | */ 124 | public null|int $returnedCount = null; 125 | /** 126 | * @var null|object 127 | * @ignore 128 | */ 129 | public null|object $responseBody = null; 130 | /** 131 | * @var null|int 132 | * @ignore 133 | */ 134 | public null|int $httpStatus = null; 135 | /** 136 | * @var int 137 | * @ignore 138 | */ 139 | public int $errorCode; 140 | /** 141 | * @var null|string 142 | * @ignore 143 | */ 144 | public null|string $errorMessage = ""; 145 | /** 146 | * @var bool 147 | * @ignore 148 | */ 149 | public bool $keepAuth = false; 150 | 151 | /** 152 | * @var bool 153 | * @ignore 154 | */ 155 | public bool $isDebug = false; 156 | /** 157 | * @var bool 158 | * @ignore 159 | */ 160 | public bool $isCertValidating = false; 161 | /** 162 | * @var bool 163 | * @ignore 164 | */ 165 | public bool $throwExceptionInError = true; 166 | /** 167 | * @var bool 168 | * @ignore 169 | */ 170 | public bool $useOAuth = false; 171 | /** 172 | * @var null|array 173 | * @ignore 174 | */ 175 | private null|array $fmDataSource; 176 | /** 177 | * @var null|string 178 | * @ignore 179 | */ 180 | public null|string $scriptError = ""; 181 | /** 182 | * @var null|string 183 | * @ignore 184 | */ 185 | public null|string $scriptResult = ""; 186 | /** 187 | * @var null|string 188 | * @ignore 189 | */ 190 | public null|string $scriptErrorPrerequest = ""; 191 | /** 192 | * @var null|string 193 | * @ignore 194 | */ 195 | public null|string $scriptResultPrerequest = ""; 196 | /** 197 | * @var null|string 198 | * @ignore 199 | */ 200 | public null|string $scriptErrorPresort = ""; 201 | /** 202 | * @var null|string 203 | * @ignore 204 | */ 205 | public null|string $scriptResultPresort = ""; 206 | /** 207 | * @var null|int 208 | * @ignore 209 | */ 210 | public null|int $timeout = null; 211 | /** 212 | * @var bool 213 | * @ignore 214 | */ 215 | public bool $fieldHTMLEncoding = false; 216 | 217 | /** 218 | * CommunicationProvider constructor. 219 | * @param string $solution 220 | * @param string $user 221 | * @param string $password 222 | * @param string|null $host 223 | * @param string|null $port 224 | * @param string|null $protocol 225 | * @param array|null $fmDataSource 226 | * @ignore 227 | */ 228 | public function __construct(string $solution, 229 | string $user, 230 | string $password, 231 | string|null $host = null, 232 | string|null $port = null, 233 | string|null $protocol = null, 234 | array|null $fmDataSource = null) 235 | { 236 | $this->solution = rawurlencode($solution); 237 | $this->user = $user; 238 | $this->password = $password; 239 | if (!is_null($host)) { 240 | if ($host == "localserver") { 241 | $this->host = "127.0.0.1"; 242 | $this->port = "3000"; 243 | $this->isLocalServer = true; 244 | $this->protocol = "http"; 245 | } else { 246 | $this->host = $host; 247 | if (!is_null($port)) { 248 | $this->port = $port; 249 | } 250 | if (!is_null($protocol)) { 251 | $this->protocol = $protocol; 252 | } 253 | } 254 | } 255 | $this->fmDataSource = $fmDataSource; 256 | $this->errorCode = -1; 257 | } 258 | 259 | /** 260 | * @param array $params Array to build the API path. Ex: `["layouts" => null]` or `["sessions" => $this->accessToken]`. 261 | * @param string|array|null $request The query parameters as `"key" => "value"`. 262 | * @param string $methodLower The method in lowercase. Ex: `"get"`, `"delete"`, etc. 263 | * @param bool $isSystem If the query is for the system (sessions, databases, etc.) or for a database. 264 | * @param string|null|false $directPath If we don't want to build the path with the other parameters, you can provide the direct path. 265 | * @return string 266 | * @ignore 267 | */ 268 | public function getURL(array $params, 269 | string|array|null $request, 270 | string $methodLower, 271 | bool $isSystem = false, 272 | string|null|false $directPath = null): string 273 | { 274 | $vStr = $this->vNum < 1 ? 'Latest' : strval($this->vNum); 275 | $url = "$this->protocol://$this->host:$this->port"; 276 | if (!empty($directPath)) { 277 | $url .= $directPath; 278 | } else { 279 | $url .= "/fmi/data/v$vStr" . ((!$isSystem) ? "/databases/$this->solution" : ""); 280 | } 281 | foreach ($params as $key => $value) { 282 | $url .= "/$key" . (is_null($value) ? "" : "/$value"); 283 | } 284 | if (!empty($request) && in_array($methodLower, array('get', 'delete'))) { 285 | $url .= '?'; 286 | foreach ($request as $key => $value) { 287 | if (key($request) !== $key) { 288 | $url .= '&'; 289 | } 290 | if ($key === 'sort' && is_array($value)) { 291 | $sortParam = $this->_buildSortParameters($value); 292 | if ($sortParam !== '[]') { 293 | $url .= '_' . $key . '=' . $sortParam; 294 | } 295 | } elseif ($key === 'limit' || $key === 'offset') { 296 | $url .= '_' . $key . '=' . (is_array($value) ? json_encode($value) : $value); 297 | } else { 298 | // handling portal object name etc. 299 | $url .= $key . '=' . (is_array($value) ? $this->_json_urlencode($value) : $value); 300 | } 301 | } 302 | } 303 | return $url; 304 | } 305 | 306 | /** 307 | * @param bool $isAddToken 308 | * @param array|null $addHeader 309 | * @return array 310 | * @ignore 311 | */ 312 | public function getHeaders(bool $isAddToken, array|null $addHeader): array 313 | { 314 | $header = []; 315 | if ($this->isLocalServer) { 316 | $header[] = 'X-Forwarded-For: 127.0.0.1'; 317 | $host = filter_input(INPUT_SERVER, 'HTTP_HOST', FILTER_SANITIZE_URL); 318 | if ($host === null || $host === false) { 319 | $host = 'localhost'; 320 | } 321 | $header[] = 'X-Forwarded-Host: ' . $host; 322 | } 323 | if ($this->useOAuth) { 324 | $header[] = "X-FM-Data-Login-Type: oauth"; 325 | } 326 | if ($isAddToken) { 327 | $header[] = "Authorization: Bearer $this->accessToken"; 328 | } 329 | if (!is_null($addHeader)) { 330 | foreach ($addHeader as $key => $value) { 331 | $header[] = "$key: $value"; 332 | } 333 | } 334 | return $header; 335 | } 336 | 337 | /** 338 | * @param array|null $request 339 | * @return array 340 | * @ignore 341 | */ 342 | public function justifyRequest(array|null $request): array 343 | { 344 | $result = $request; 345 | // cast a number 346 | if (isset($result['fieldData'])) { 347 | foreach ($result['fieldData'] as $fieldName => $fieldValue) { 348 | $result['fieldData'][$fieldName] = (string)$fieldValue; 349 | } 350 | } 351 | if (isset($result['query'])) { 352 | foreach ($result['query'] as $key => $array) { 353 | foreach ($array as $fieldName => $fieldValue) { 354 | if (!is_array($fieldValue)) { 355 | $result['query'][$key][$fieldName] = (string)$fieldValue; 356 | } 357 | } 358 | } 359 | } 360 | 361 | if (isset($result['sort'])) { 362 | $sort = []; 363 | foreach ($result['sort'] as $sortCondition) { 364 | if (isset($sortCondition[0])) { 365 | $sortOrder = 'ascend'; 366 | if (isset($sortCondition[1])) { 367 | $sortOrder = $this->adjustSortDirection($sortCondition[1]); 368 | } 369 | $sort[] = ['fieldName' => $sortCondition[0], 'sortOrder' => $sortOrder]; 370 | } 371 | } 372 | $result['sort'] = $sort; 373 | } 374 | return $result; 375 | } 376 | 377 | /** 378 | * @return object|null 379 | * @throws Exception In case of any error, an exception arises. 380 | * @ignore 381 | */ 382 | public function getProductInfo(): object|null 383 | { 384 | $returnValue = null; 385 | $params = ["productInfo" => null]; 386 | $request = []; 387 | try { 388 | $this->callRestAPI($params, false, "GET", $request, null, true); // Throw Exception 389 | $this->storeToProperties(); 390 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 391 | $returnValue = $this->responseBody->response->productInfo; 392 | } 393 | } catch (Exception $e) { 394 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 395 | $returnValue = array("version" => 17); 396 | } else { 397 | throw $e; 398 | } 399 | } 400 | return $returnValue; 401 | } 402 | 403 | /** 404 | * @return array|null 405 | * @throws Exception In case of any error, an exception arises. 406 | * @ignore 407 | */ 408 | public function getDatabaseNames(): array|null 409 | { 410 | $returnValue = null; 411 | if ($this->useOAuth) { 412 | $headers = [ 413 | "Content-Type" => "application/json", 414 | "X-FM-Data-OAuth-Request-Id" => "{$this->user}", 415 | "X-FM-Data-OAuth-Identifier" => "{$this->password}", 416 | ]; 417 | } else { 418 | $value = "Basic " . base64_encode("{$this->user}:{$this->password}"); 419 | $headers = ["Content-Type" => "application/json", "Authorization" => $value]; 420 | } 421 | $params = ["databases" => null]; 422 | $request = []; 423 | $this->callRestAPI($params, false, "GET", $request, $headers, true); // Throw Exception 424 | $this->storeToProperties(); 425 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 426 | $returnValue = $this->responseBody->response->databases; 427 | } 428 | $this->logout(); 429 | return $returnValue; 430 | } 431 | 432 | /** 433 | * @return null|array 434 | * @throws Exception In case of any error, an exception arises. 435 | * @ignore 436 | */ 437 | public function getLayoutNames(): null|array 438 | { 439 | $returnValue = null; 440 | if ($this->login()) { 441 | $params = ["layouts" => null]; 442 | $request = []; 443 | $headers = []; 444 | $this->callRestAPI($params, true, "GET", $request, $headers); // Throw Exception 445 | $this->storeToProperties(); 446 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 447 | $returnValue = $this->responseBody->response->layouts; 448 | } 449 | $this->logout(); 450 | } 451 | return $returnValue; 452 | } 453 | 454 | /** 455 | * @throws Exception In case of any error, an exception arises. 456 | * @ignore 457 | */ 458 | public function getScriptNames(): null|array 459 | { 460 | $returnValue = null; 461 | if ($this->login()) { 462 | $params = ["scripts" => null]; 463 | $request = []; 464 | $headers = []; 465 | $this->callRestAPI($params, true, "GET", $request, $headers); // Throw Exception 466 | $this->storeToProperties(); 467 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 468 | $returnValue = $this->responseBody->response->scripts; 469 | } 470 | $this->logout(); 471 | } 472 | return $returnValue; 473 | } 474 | 475 | /** 476 | * @return bool 477 | * @throws Exception In case of any error, an exception arises. 478 | * @ignore 479 | */ 480 | public function login(): bool 481 | { 482 | if ($this->keepAuth || !is_null($this->accessToken)) { 483 | return true; 484 | } 485 | 486 | if ($this->useOAuth) { 487 | $headers = [ 488 | "Content-Type" => "application/json", 489 | "X-FM-Data-OAuth-Request-Id" => "{$this->user}", 490 | "X-FM-Data-OAuth-Identifier" => "{$this->password}", 491 | ]; 492 | } else { 493 | $value = "Basic " . base64_encode("{$this->user}:{$this->password}"); 494 | $headers = [ 495 | "Content-Type" => "application/json", 496 | "Authorization" => $value 497 | ]; 498 | } 499 | $params = ["sessions" => null]; 500 | $request = []; 501 | $request["fmDataSource"] = (!is_null($this->fmDataSource)) ? $this->fmDataSource : []; 502 | try { 503 | $this->callRestAPI($params, false, "POST", $request, $headers); // Throw Exception 504 | $this->storeToProperties(); 505 | if ($this->httpStatus == 200 && $this->errorCode == 0) { 506 | $this->accessToken = $this->responseBody->response->token; 507 | return true; 508 | } 509 | } catch (Exception $e) { 510 | $this->accessToken = null; 511 | throw $e; 512 | } 513 | return false; 514 | } 515 | 516 | /** 517 | * 518 | * @return void 519 | * @throws Exception In case of any error, an exception arises. 520 | * @ignore 521 | */ 522 | public function logout(): void 523 | { 524 | if ($this->keepAuth) { 525 | return; 526 | } 527 | $params = ["sessions" => $this->accessToken]; 528 | $this->callRestAPI($params, true, "DELETE"); // Throw Exception 529 | $this->accessToken = null; 530 | } 531 | 532 | /** 533 | * @return array|null 534 | * @ignore 535 | */ 536 | private function getSupportingProviders(): null|array 537 | { 538 | try { 539 | $this->callRestAPI([], true, 'GET', [], [], 540 | false, "/fmws/oauthproviderinfo"); // Throw Exception 541 | $result = []; 542 | // foreach ($this->responseBody as $key => $item) { 543 | // 544 | // } 545 | return $result; 546 | } catch (Exception $ex) { 547 | return null; 548 | } 549 | } 550 | 551 | /** 552 | * @param $provider 553 | * @return string|array|null 554 | * @ignore 555 | */ 556 | private function getOAuthIdentifier($provider): string|array|null 557 | { 558 | try { 559 | $this->callRestAPI( 560 | [], false, 'GET', 561 | [ 562 | "trackingID" => rand(10000000, 99999999), 563 | "provider" => $provider, 564 | "address" => "127.0.0.1", 565 | "X-FMS-OAuth-AuthType" => 2 566 | ], 567 | [ 568 | "X-FMS-Application-Type" => 9, 569 | "X-FMS-Application-Version" => 15, 570 | "X-FMS-Return-URL" => "http://127.0.0.1/", 571 | ], 572 | false, "/oauth/getoauthurl" 573 | ); // Throw Exception 574 | $result = []; 575 | // foreach ($this->responseBody as $key => $item) { 576 | // 577 | // } 578 | return $result; 579 | } catch (Exception $ex) { 580 | return null; 581 | } 582 | } 583 | 584 | /** 585 | * @param array $params 586 | * @param bool $isAddToken 587 | * @param string $method 588 | * @param string|array|null $request 589 | * @param array|null $addHeader 590 | * @param bool $isSystem for Metadata 591 | * @param string|null|false $directPath 592 | * @return void 593 | * @throws Exception In case of any error, an exception arises. 594 | * @ignore 595 | */ 596 | public function callRestAPI(array $params, 597 | bool $isAddToken, 598 | string $method = 'GET', 599 | string|array|null $request = null, 600 | array|null $addHeader = null, 601 | bool $isSystem = false, 602 | string|null|false $directPath = null): void 603 | { 604 | $methodLower = strtolower($method); 605 | $url = $this->getURL($params, $request, $methodLower, $isSystem, $directPath); 606 | $header = $this->getHeaders($isAddToken, $addHeader); 607 | $jsonEncoding = true; 608 | if (is_string($request)) { 609 | $jsonEncoding = false; 610 | } elseif ($methodLower !== 'get' && !is_null($request)) { 611 | $request = $this->justifyRequest($request); 612 | } 613 | $ch = $this->_createCurlHandle($url); 614 | curl_setopt($ch, CURLOPT_VERBOSE, 0); 615 | curl_setopt($ch, CURLOPT_HEADER, 1); 616 | curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); 617 | if ($methodLower == 'post') { 618 | curl_setopt($ch, CURLOPT_POST, 1); 619 | } elseif (in_array($methodLower, ['put', 'patch', 'delete', 'get'], true)) { 620 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($methodLower)); 621 | } 622 | curl_setopt($ch, CURLOPT_HTTPHEADER, $header); 623 | if ($methodLower != 'get') { 624 | if ($jsonEncoding) { 625 | if ($methodLower === 'post' && isset($request['fieldData']) && $request['fieldData'] === [] 626 | ) { 627 | // create an empty record 628 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request, JSON_FORCE_OBJECT)); 629 | } else { 630 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request)); 631 | } 632 | } else { 633 | curl_setopt($ch, CURLOPT_POSTFIELDS, $request); 634 | } 635 | } 636 | $response = curl_exec($ch); 637 | $this->curlInfo = curl_getinfo($ch); 638 | $this->curlErrorNumber = curl_errno($ch); 639 | if ($this->curlErrorNumber) { 640 | $this->curlError = curl_error($ch); 641 | } 642 | curl_close($ch); 643 | 644 | $this->method = $method; 645 | $this->url = $url; 646 | $this->requestHeader = $header; 647 | $this->requestBody = ($methodLower != 'get') ? $request : null; 648 | $this->responseHeader = substr($response, 0, $this->curlInfo["header_size"]); 649 | $this->responseBody = json_decode(substr($response, $this->curlInfo["header_size"]), false, 512, JSON_BIGINT_AS_STRING); 650 | 651 | if ($this->isDebug) { 652 | $this->debugOutput(); 653 | } 654 | if ($this->throwExceptionInError) { 655 | $httpStatus = $this->getCurlInfo("http_code"); 656 | $errorCode = $this->responseBody && property_exists($this->responseBody->messages[0], 'code') ? 657 | intval($this->responseBody->messages[0]->code) : -1; 658 | $errorMessage = $this->responseBody && property_exists($this->responseBody->messages[0], 'message') ? 659 | $this->responseBody->messages[0]->message : 'ERROR'; 660 | $description = ''; 661 | if ($this->curlErrorNumber > 0) { 662 | $description .= "cURL in PHP / Error Code: {$this->curlErrorNumber}, Error Message: {$this->curlError}. "; 663 | } else { 664 | if ($httpStatus !== 200) { 665 | $description .= "HTTP Status Code: {$httpStatus}. "; 666 | } 667 | if ($errorCode > 0) { 668 | $description .= "FileMaker Data API / Error Code: {$errorCode}, Error Message: {$errorMessage}. "; 669 | } 670 | } 671 | if ($description !== '') { 672 | $description = date('Y-m-d H:i:s ') . "{$description}"; 673 | $description .= "[URL({$this->method}): {$this->url}]"; 674 | if ($errorCode !== 401) { 675 | throw new Exception($description, $errorCode); 676 | } 677 | } 678 | } 679 | } 680 | 681 | /** 682 | * Return the base64 encoded data in container field. 683 | * Thanks to 'base64bits' as https://github.com/msyk/FMDataAPI/issues/18. 684 | * @param string $url 685 | * @return string The base64 encoded data in container field. 686 | * @throws Exception 687 | * @ignore 688 | */ 689 | public function accessToContainer(string $url): string 690 | { 691 | $cookieFile = tempnam(sys_get_temp_dir(), "CURLCOOKIE"); // Create a cookie file. 692 | 693 | // Visit the container URL to set the cookie. 694 | $ch = $this->_createCurlHandle($url); 695 | curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile); 696 | curl_exec($ch); 697 | if (curl_errno($ch) !== 0) { 698 | $errMsg = curl_error($ch); 699 | curl_close($ch); 700 | throw new Exception("Error in creating cookie file. {$errMsg}"); 701 | } 702 | curl_close($ch); 703 | 704 | // Visit the container URL again. 705 | $ch = $this->_createCurlHandle($url); 706 | curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile); 707 | $output = curl_exec($ch); 708 | if (curl_errno($ch) !== 0) { 709 | $errMsg = curl_error($ch); 710 | curl_close($ch); 711 | throw new Exception("Error in downloading content of file. {$errMsg}"); 712 | } 713 | curl_close($ch); 714 | 715 | return base64_encode($output); // Process the data as needed. 716 | } 717 | 718 | /** 719 | * @ignore 720 | */ 721 | public function storeToProperties(): void 722 | { 723 | $this->httpStatus = 0; 724 | $this->errorCode = -1; 725 | $this->scriptError = null; 726 | $this->scriptResult = null; 727 | $this->scriptErrorPrerequest = null; 728 | $this->scriptResultPrerequest = null; 729 | $this->scriptErrorPresort = null; 730 | $this->scriptResultPresort = null; 731 | $this->targetTable = null; 732 | $this->totalCount = null; 733 | $this->foundCount = null; 734 | $this->returnedCount = null; 735 | $this->errorMessage = null; 736 | 737 | if (property_exists($this, 'responseBody')) { 738 | $rbody = $this->responseBody; 739 | if (is_object($rbody)) { 740 | if (property_exists($rbody, 'messages')) { 741 | $result = $rbody->messages[0]; 742 | $this->httpStatus = $this->getCurlInfo("http_code"); 743 | $this->errorCode = property_exists($result, 'code') ? $result->code : -1; 744 | $this->errorMessage = property_exists($result, 'message') && $result->code != 0 ? $result->message : null; 745 | } 746 | if (property_exists($rbody, 'response')) { 747 | $result = $rbody->response; 748 | $this->scriptError = property_exists($result, 'scriptError') ? $result->scriptError : null; 749 | $this->scriptResult = property_exists($result, 'scriptResult') ? $result->scriptResult : null; 750 | $this->scriptErrorPrerequest = property_exists($result, 'scriptError.prerequest') ? 751 | $result->{'scriptError.prerequest'} : null; 752 | $this->scriptResultPrerequest = property_exists($result, 'scriptResult.prerequest') ? 753 | $result->{'scriptResult.prerequest'} : null; 754 | $this->scriptErrorPresort = property_exists($result, "scriptError.presort") ? 755 | $result->{"scriptError.presort"} : null; 756 | $this->scriptResultPresort = property_exists($result, "scriptResult.presort") ? 757 | $result->{"scriptResult.presort"} : null; 758 | if (property_exists($result, 'dataInfo')) { 759 | $dataInfo = $result->dataInfo; 760 | $this->targetTable = property_exists($dataInfo, 'table') ? 761 | $dataInfo->table : null; 762 | $this->totalCount = property_exists($dataInfo, 'totalRecordCount') ? 763 | $dataInfo->totalRecordCount : null; 764 | $this->foundCount = property_exists($dataInfo, 'foundCount') ? 765 | $dataInfo->foundCount : null; 766 | $this->returnedCount = property_exists($dataInfo, 'returnedCount') ? 767 | $dataInfo->returnedCount : null; 768 | } 769 | } 770 | } 771 | } 772 | } 773 | 774 | /** 775 | * @param string $direction 776 | * @return string 777 | * @ignore 778 | */ 779 | public function adjustSortDirection(string $direction): string 780 | { 781 | if (strtoupper($direction) == 'ASC') { 782 | $direction = 'ascend'; 783 | } elseif (strtoupper($direction) == 'DESC') { 784 | $direction = 'descend'; 785 | } 786 | 787 | return $direction; 788 | } 789 | 790 | /** 791 | * @param $key 792 | * @return mixed 793 | * @ignore 794 | */ 795 | public function getCurlInfo($key): mixed 796 | { 797 | return $this->curlInfo[$key]; 798 | } 799 | 800 | /** 801 | * @param bool $isReturnValue 802 | * @return string 803 | * @ignore 804 | */ 805 | public function debugOutput(bool $isReturnValue = false): string 806 | { 807 | $str = "
URL: "; 808 | $str .= $this->method . ' ' . htmlspecialchars($this->url); 809 | $str .= "
Added Request Header:
";
810 |         $str .= htmlspecialchars(var_export($this->requestHeader, true));
811 |         $str .= "

Request Body:
";
812 |         if (is_string($this->requestBody)) {
813 |             $str .= htmlspecialchars(substr($this->requestBody, 0, 40));
814 |         } else {
815 |             $str .= htmlspecialchars(json_encode($this->requestBody, JSON_PRETTY_PRINT));
816 |         }
817 |         $str .= "

Response Header:
";
818 |         $str .= htmlspecialchars($this->responseHeader);
819 |         $str .= "

Response Body:
";
820 |         $str .= htmlspecialchars(json_encode($this->responseBody, JSON_PRETTY_PRINT));
821 |         //$str .= "

Info:
";
822 |         //$str .= var_export($this->curlInfo, true);
823 |         $str .= "

CURL ErrorNumber: {$this->curlErrorNumber}"; 824 | $str .= "
CURL Error: "; 825 | $str .= $this->curlError ? htmlspecialchars($this->curlError) : ''; 826 | $str .= "
"; 827 | if ($isReturnValue) { 828 | return $str; 829 | } else { 830 | echo $str; 831 | } 832 | return ""; 833 | } 834 | 835 | /** 836 | * @param array $value 837 | * @return string 838 | * @ignore 839 | */ 840 | private function _buildSortParameters(array $value): string 841 | { 842 | $param = '['; 843 | foreach ($value as $sortCondition) { 844 | if (isset($sortCondition[0])) { 845 | if ($param !== '[') { 846 | $param .= ','; 847 | } 848 | if (isset($sortCondition[1])) { 849 | $sortOrder = $this->adjustSortDirection($sortCondition[1]); 850 | $param .= '{"fieldName":' . json_encode($sortCondition[0]) . 851 | ',"sortOrder":' . json_encode($sortOrder) . '}'; 852 | } else { 853 | $param .= '{"fieldName":' . json_encode($sortCondition[0]) . '}'; 854 | } 855 | } 856 | } 857 | $param .= ']'; 858 | 859 | return $param; 860 | } 861 | 862 | /** 863 | * @param array $value 864 | * @return string 865 | * @ignore 866 | */ 867 | private function _json_urlencode(array $value): string 868 | { 869 | $str = '['; 870 | if (count($value) > 0) { 871 | foreach ($value as $el) { 872 | if ($str !== '[') { 873 | $str .= ','; 874 | } 875 | $str .= '"' . urlencode($el) . '"'; 876 | } 877 | } 878 | $str .= ']'; 879 | 880 | return $str; 881 | } 882 | 883 | /** 884 | * To create and configure cURL at a single place, avoiding code redundancy. 885 | * 886 | * @param string $url The URL you want to access. 887 | * @return CurlHandle 888 | */ 889 | private function _createCurlHandle(string $url): CurlHandle 890 | { 891 | $ch = curl_init(); 892 | curl_setopt($ch, CURLOPT_URL, $url); 893 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 894 | curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_DEFAULT); 895 | if ($this->isCertValidating) { 896 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 897 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); 898 | /* Use the OS native certificate authorities, if possible. 899 | This fixes SSL validation errors if `php.ini` doesn't have [curl] `curl.cainfo`, 900 | set properly of if this PEM file isn't up to date. 901 | Better rely on the OS certificate authorities, which is maintained automatically. */ 902 | if (defined('CURLSSLOPT_NATIVE_CA') 903 | && version_compare(curl_version()['version'], '7.71', '>=')) { 904 | curl_setopt($ch, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); 905 | } 906 | } else { 907 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); 908 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 909 | } 910 | if (!is_null($this->timeout)) { 911 | curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); 912 | } 913 | return $ch; 914 | } 915 | } 916 | -------------------------------------------------------------------------------- /src/Supporting/FileMakerLayout.php: -------------------------------------------------------------------------------- 1 | 16 | * @copyright 2017-2024 Masayuki Nii (Claris FileMaker is registered trademarks of Claris International Inc. in the U.S. and other countries.) 17 | */ 18 | class FileMakerLayout 19 | { 20 | /** 21 | * @var CommunicationProvider|null The instance of the communication class. 22 | * @ignore 23 | */ 24 | private CommunicationProvider|null $restAPI; 25 | /** 26 | * @var null|string 27 | * @ignore 28 | */ 29 | private string|null $layout; 30 | 31 | /** 32 | * FileMakerLayout constructor. 33 | * @param CommunicationProvider|null $restAPI 34 | * @param string $layout 35 | * @ignore 36 | */ 37 | public function __construct(CommunicationProvider|null $restAPI, 38 | string $layout) 39 | { 40 | $this->restAPI = $restAPI; 41 | $this->layout = $layout; 42 | } 43 | 44 | /** 45 | * Start a transaction which is a serial calling of any database operations, 46 | * and login with the target layout. 47 | * @throws Exception 48 | */ 49 | public function startCommunication(): void 50 | { 51 | if ($this->restAPI->login()) { 52 | $this->restAPI->keepAuth = true; 53 | } 54 | } 55 | 56 | /** 57 | * Finish a transaction which is a serial calling of any database operations, and logout. 58 | * @throws Exception 59 | */ 60 | public function endCommunication(): void 61 | { 62 | $this->restAPI->keepAuth = false; 63 | $this->restAPI->logout(); 64 | } 65 | 66 | /** 67 | * @param array|null $param 68 | * @param bool|string $shortKey 69 | * @param string $method 70 | * @return array 71 | * @ignore 72 | */ 73 | private function buildPortalParameters(array|null $param, 74 | bool|string $shortKey = false, 75 | string $method = "GET"): array 76 | { 77 | $key = $shortKey ? "portal" : "portalData"; 78 | $prefix = $method === "GET" ? "" : "_"; 79 | $request = []; 80 | if (array_values($param) === $param) { 81 | $request[$key] = $param; 82 | } else { 83 | $request[$key] = array_keys($param); 84 | foreach ($param as $portalName => $options) { 85 | if (!is_null($options) && $options['limit']) { 86 | $request["{$prefix}limit.{$portalName}"] = $options['limit']; 87 | } 88 | if (!is_null($options) && $options['offset']) { 89 | $request["{$prefix}offset.{$portalName}"] = $options['offset']; 90 | } 91 | } 92 | } 93 | return $request; 94 | } 95 | 96 | /** 97 | * @param array|null $param 98 | * @return array 99 | * @ignore 100 | */ 101 | private function buildScriptParameters(array|null $param): array 102 | { 103 | $request = []; 104 | $scriptKeys = [ 105 | "script", "script.param", "script.prerequest", "script.prerequest.param", 106 | "script.presort", "script.presort.param", "layout.response" 107 | ]; 108 | foreach ($scriptKeys as $key) { 109 | if (isset($param[$key])) { 110 | $request[$key] = $param[$key]; 111 | } 112 | } 113 | if (!empty($request)) { 114 | switch (count($request)) { 115 | case 1: 116 | $request["script"] = $param[0]; 117 | break; 118 | case 2: 119 | $request["script"] = $param[0]; 120 | $request["layout.response"] = $param[1]; 121 | break; 122 | case 3: 123 | $request["script"] = $param[0]; 124 | $request["script.param"] = $param[1]; 125 | $request["layout.response"] = $param[2]; 126 | break; 127 | case 4: 128 | $request["script.prerequest"] = $param[0]; 129 | $request["script.presort"] = $param[1]; 130 | $request["script"] = $param[2]; 131 | $request["layout.response"] = $param[3]; 132 | break; 133 | } 134 | } 135 | return $request; 136 | } 137 | 138 | /** 139 | * Query to the FileMaker Database and returns the result as FileMakerRelation object. 140 | * @param array|null $condition The array of associated array which has a field name and "omit" keys as like: 141 | * array(array("FamilyName"=>"Nii*", "Country"=>"Japan")). 142 | * In this example of apply the AND operation for two fields, 143 | * and "FamilyName" and "Country" are field name. The value can contain the operator: 144 | * =, ==, !, <, ≤ or <=, >, ≥ or >=, ..., //, ?, @, #, *, \, "", ~. 145 | * If you want to apply the OR operation, describe array of array as like: 146 | * array(array("FamilyName"=>"Nii*"), array("Country"=>"Japan")). 147 | * If you want to omit record match with condition set the "omit" element as like: 148 | * array("FamilyName"=>"Nii*", "omit"=>"true"). 149 | * If you want to query all records in the layout, set the first parameter to null. 150 | * @param array|null $sort The array of array which has two elements as a field name and order key: 151 | * array(array("FamilyName", "ascend"), array("GivenName", "descend")). 152 | * The value of order key can be 'ascend', 'descend' or value list name. The default value is 'ascend'. 153 | * @param int $offset The start number of the record set, and the first record is 1, but the number 0 154 | * queries from the first record. The default value is 0. 155 | * @param int $range The number of records contains in the result record set. The default value is 100. 156 | * @param array|null $portal The array of the portal's object names. The query result is going to contain portals 157 | * specified in this parameter. If you want to include all portals, set it null or omit it. 158 | * Simple case is array('portal1', portal2'), and just includes two portals named 'portal1' and 'portal2' 159 | * in the query result. If you set the range of records to a portal, you have to build an associated array as like: 160 | * array('portal1' => array('offset'=>1,'limit'=>5), 'portal2' => null). The record 1 to 5 of portal1 include 161 | * the query result, and also all records in portal2 do. 162 | * @param array|null $script scripts that should execute the right timings. 163 | * The most understandable description is an associated array with API's keywords "script", "script.param", 164 | * "script.prerequest", "script.prerequest.param", "script.presort", "script.presort.param", "layout.response." 165 | * These keywords have to be a key, and the value is script name or script parameter, 166 | * ex. {"script"=>"StartingOver", "script.param"=>"344|21|abcd"}. 167 | * If $script is array with one element, it's handled as the value of "script." 168 | * If $script is array with two elements, these are handled as values of "script" and "layout.response." 169 | * If it's three elements, these are "script", "script.param" and "layout.response." 170 | * If it's four elements, these are "script.prerequest", "script.presort", "script" and "layout.response." 171 | * @param int|null $dateformats Use this option to specify date formats for date, time, and timestamp fields. The relevant values are: 0 for US, 1 for file locale, or 2 for ISO8601 172 | * @return FileMakerRelation|null Query result. 173 | * @throws Exception In case of any error, an exception arises. 174 | */ 175 | public function query(array|null $condition = null, 176 | array|null $sort = null, 177 | int $offset = 0, 178 | int $range = 0, 179 | array|null $portal = null, 180 | array|null $script = null, 181 | int|null $dateformats = null): FileMakerRelation|null 182 | { 183 | if ($this->restAPI->login()) { 184 | $headers = ["Content-Type" => "application/json"]; 185 | $request = []; 186 | $method = is_null($condition) ? "GET" : "POST"; 187 | if (!is_null($sort)) { 188 | $request["sort"] = $sort; 189 | } 190 | if ($offset > 0) { 191 | $request["offset"] = (string)$offset; 192 | } 193 | if ($range > 0) { 194 | $request["limit"] = (string)$range; 195 | } 196 | if (!is_null($portal)) { 197 | $request = array_merge($request, $this->buildPortalParameters($portal, true, $method)); 198 | } 199 | if (!is_null($script)) { 200 | $request = array_merge($request, $this->buildScriptParameters($script)); 201 | } 202 | if (!is_null($condition)) { 203 | $request["query"] = $condition; 204 | $params = ["layouts" => $this->layout, "_find" => null]; 205 | } else { 206 | $params = ["layouts" => $this->layout, "records" => null]; 207 | } 208 | if (!is_null($dateformats)) { 209 | $request["dateformats"] = $dateformats; 210 | } 211 | $this->restAPI->callRestAPI($params, true, $method, $request, $headers); // Throw Exception 212 | $this->restAPI->storeToProperties(); 213 | $result = $this->restAPI->responseBody; 214 | $fmrel = null; 215 | if ($result && $result->response && 216 | property_exists($result->response, 'data') && 217 | property_exists($result, 'messages') 218 | ) { 219 | $fmrel = new FileMakerRelation($result->response->data, 220 | property_exists($result->response, 'dataInfo') ? $result->response->dataInfo : null, 221 | "OK", $result->messages[0]->code, null, $this->restAPI); 222 | } 223 | $this->restAPI->logout(); 224 | return $fmrel; 225 | } else { 226 | return null; 227 | } 228 | } 229 | 230 | /** 231 | * Query to the FileMaker Database with recordId special field and returns the result as FileMakerRelation object. 232 | * @param int|null $recordId The recordId. 233 | * @param array|null $portal See the query() method's same parameter. 234 | * @param array|null $script scripts that should execute the right timings. See FileMakerRelation::query(). 235 | * @return FileMakerRelation|null Query result. 236 | * @throws Exception In case of any error, an exception arises. 237 | */ 238 | public function getRecord(int|null $recordId, 239 | array|null $portal = null, 240 | array|null $script = null): FileMakerRelation|null 241 | { 242 | if (is_null($recordId)) { 243 | return null; 244 | } 245 | if ($this->restAPI->login()) { 246 | $request = []; 247 | if (!is_null($portal)) { 248 | $request = array_merge($request, $this->buildPortalParameters($portal, true)); 249 | } 250 | if (!is_null($script)) { 251 | $request = array_merge($request, $this->buildScriptParameters($script)); 252 | } 253 | $headers = ["Content-Type" => "application/json"]; 254 | $params = ["layouts" => $this->layout, "records" => $recordId]; 255 | $this->restAPI->callRestAPI($params, true, "GET", $request, $headers); // Throw Exception 256 | $this->restAPI->storeToProperties(); 257 | $result = $this->restAPI->responseBody; 258 | $fmrel = null; 259 | if ($result) { 260 | $dataInfo = null; 261 | if (property_exists($result->response, 'dataInfo') && is_object($result->response->dataInfo)) { 262 | $dataInfo = clone $result->response->dataInfo; 263 | $dataInfo->returnedCount = 1; 264 | } 265 | $fmrel = new FileMakerRelation($result->response->data, $dataInfo, 266 | "OK", $result->messages[0]->code, null, $this->restAPI); 267 | } 268 | $this->restAPI->logout(); 269 | return $fmrel; 270 | } else { 271 | return null; 272 | } 273 | } 274 | 275 | /** 276 | * Create a record on the target layout of the FileMaker database. 277 | * @param array|null $data Associated array contains the initial values. 278 | * Keys are field names and values is these initial values. 279 | * @param array|null $portal Associated array contains the modifying values in the portal. 280 | * Ex.: {""=>{""=>""...}}. FieldName has to "::". 281 | * @param array|null $script scripts that should execute the right timings. See FileMakerRelation::query(). 282 | * @return int|null The recordId of created record. 283 | * If the returned value is an integer larger than 0, it shows one record was created. 284 | * @throws Exception In case of any error, an exception arises. 285 | */ 286 | public function create(array|null $data = null, 287 | array|null $portal = null, 288 | array|null $script = null): int|null 289 | { 290 | if ($this->restAPI->login()) { 291 | $headers = ["Content-Type" => "application/json"]; 292 | $params = ["layouts" => $this->layout, "records" => null]; 293 | $request = ["fieldData" => is_null($data) ? [] : $data]; 294 | if (!is_null($portal)) { 295 | $request = array_merge($request, ["portalData" => $portal]); 296 | } 297 | if (!is_null($script)) { 298 | $request = array_merge($request, $this->buildScriptParameters($script)); 299 | } 300 | $this->restAPI->callRestAPI($params, true, "POST", $request, $headers); // Throw Exception 301 | $result = $this->restAPI->responseBody; 302 | $this->restAPI->storeToProperties(); 303 | $this->restAPI->logout(); 304 | return $result->response->recordId; 305 | } else { 306 | return null; 307 | } 308 | } 309 | 310 | /** 311 | * Duplicate the record. 312 | * @param int|null $recordId The valid recordId value to duplicate. 313 | * @param array|null $script scripts that should execute the right timings. See FileMakerRelation::query(). 314 | * @throws Exception In case of any error, an exception arises. 315 | */ 316 | public function duplicate(int|null $recordId, 317 | array|null $script = null): void 318 | { 319 | if (is_null($recordId)) { 320 | return; 321 | } 322 | if ($this->restAPI->login()) { 323 | $request = "{}"; //FileMaker expects an empty object, so we have to set "{}" here 324 | $headers = ["Content-Type" => "application/json"]; 325 | $params = ['layouts' => $this->layout, 'records' => $recordId]; 326 | if (!is_null($script)) { 327 | $request = $this->buildScriptParameters($script); 328 | } 329 | $this->restAPI->callRestAPI($params, true, 'POST', $request, $headers); // Throw Exception 330 | $this->restAPI->storeToProperties(); 331 | $this->restAPI->logout(); 332 | } 333 | } 334 | 335 | /** 336 | * Delete the record. 337 | * @param int|null $recordId The valid recordId value to delete. 338 | * @param array|null $script scripts that should execute the right timings. See FileMakerRelation::query(). 339 | * @throws Exception In case of any error, an exception arises. 340 | */ 341 | public function delete(int|null $recordId, 342 | array|null $script = null): void 343 | { 344 | if (is_null($recordId)) { 345 | return; 346 | } 347 | if ($this->restAPI->login()) { 348 | $request = []; 349 | $headers = null; 350 | $params = ['layouts' => $this->layout, 'records' => $recordId]; 351 | if (!is_null($script)) { 352 | $request = $this->buildScriptParameters($script); 353 | } 354 | $this->restAPI->callRestAPI($params, true, 'DELETE', $request, $headers); // Throw Exception 355 | $this->restAPI->storeToProperties(); 356 | $this->restAPI->logout(); 357 | } 358 | } 359 | 360 | /** 361 | * Update fields in one record. 362 | * @param int|null $recordId The valid recordId value to update. 363 | * @param array|null $data Associated array contains the modifying values. 364 | * Keys are field names and values is these initial values. 365 | * @param int $modId The modId to allow updating. This parameter is for detect to modifying other users. 366 | * If you omit this parameter, update operation does not care the value of modId special field. 367 | * @param array|object|null $portal Associated array contains the modifying values in the portal. 368 | * Ex.: {""=>{""=>"", "recordId"=>"12"}}. FieldName has to "::". 369 | * The recordId key specifies the record to edit in the portal. 370 | * @param array|null $script scripts that should execute the right timings. See FileMakerRelation::query(). 371 | * @throws Exception In case of any error, an exception arises. 372 | */ 373 | public function update(int|null $recordId, 374 | array|null $data, 375 | int $modId = -1, 376 | array|object|null $portal = null, 377 | array|null $script = null): void 378 | { 379 | if (is_null($recordId)) { 380 | return; 381 | } 382 | if ($this->restAPI->login()) { 383 | $headers = ["Content-Type" => "application/json"]; 384 | $params = ["layouts" => $this->layout, "records" => $recordId]; 385 | $request = []; 386 | if (!is_null($data)) { 387 | $request = array_merge($request, ["fieldData" => $data]); 388 | } 389 | if (!is_null($portal)) { 390 | $request = array_merge($request, ["portalData" => $portal]); 391 | } 392 | if (!is_null($script)) { 393 | $request = array_merge($request, $this->buildScriptParameters($script)); 394 | } 395 | if ($modId > -1) { 396 | $request = array_merge($request, ["modId" => (string)$modId]); 397 | } 398 | $this->restAPI->callRestAPI($params, true, "PATCH", $request, $headers); // Throw exception 399 | $this->restAPI->storeToProperties(); 400 | $this->restAPI->logout(); 401 | } 402 | } 403 | 404 | /** 405 | * Set the value to the global field. 406 | * @param array $fields The Associated array contains the global field names and its values. 407 | * Keys are global field names and values is these values. 408 | * @throws Exception In case of any error, an exception arises. 409 | */ 410 | public function setGlobalField(array $fields): void 411 | { 412 | if ($this->restAPI->login()) { 413 | foreach ($fields as $name => $value) { 414 | if ((function_exists('mb_strpos') && mb_strpos($name, '::') === false) || !str_contains($name, '::')) { 415 | unset($fields[$name]); 416 | $fields[$this->layout . '::' . $name] = $value; 417 | } 418 | } 419 | $headers = ["Content-Type" => "application/json"]; 420 | $params = ["globals" => null]; 421 | $request = ["globalFields" => $fields]; 422 | $this->restAPI->callRestAPI($params, true, "PATCH", $request, $headers); // Throw exception 423 | $this->restAPI->storeToProperties(); 424 | $this->restAPI->logout(); 425 | } 426 | } 427 | 428 | /** 429 | * Upload the file into container filed. 430 | * @param string $filePath The file path to upload. 431 | * @param int|null $recordId The Record ID of the record. 432 | * @param string $containerFieldName The field name of container field. 433 | * @param int|null $containerFieldRepetition In the case of repetiton field, this has to be the number from 1. 434 | * If omitted this, the number "1" is going to be specified. 435 | * @param string|null $fileName Another file name for an uploading file. If omitted, the original file name is chosen. 436 | * @throws Exception In case of any error, an exception arises. 437 | */ 438 | public function uploadFile(string $filePath, 439 | int|null $recordId, 440 | string $containerFieldName, 441 | int|null $containerFieldRepetition = null, 442 | string|null $fileName = null): void 443 | { 444 | if (!file_exists($filePath)) { 445 | throw new Exception("File doesn't exist: {$filePath}."); 446 | } 447 | if (is_null($recordId)) { 448 | return; 449 | } 450 | if ($this->restAPI->login()) { 451 | $CRLF = chr(13) . chr(10); 452 | $DQ = '"'; 453 | $boundary = "FMDataAPI_UploadFile-" . uniqid(); 454 | $fileName = is_null($fileName) ? basename($filePath) : $fileName; 455 | $headers = ["Content-Type" => "multipart/form-data; boundary={$boundary}"]; 456 | $repNum = is_null($containerFieldRepetition) ? 1 : $containerFieldRepetition; 457 | $params = [ 458 | "layouts" => $this->layout, 459 | "records" => $recordId, 460 | "containers" => "{$containerFieldName}/{$repNum}", 461 | ]; 462 | $request = "--{$boundary}{$CRLF}"; 463 | $request .= "Content-Disposition: form-data; name={$DQ}upload{$DQ}; filename={$DQ}{$fileName}{$DQ}{$CRLF}"; 464 | $request .= $CRLF; 465 | $request .= file_get_contents($filePath); 466 | $request .= "{$CRLF}{$CRLF}--{$boundary}--{$CRLF}"; 467 | $this->restAPI->callRestAPI($params, true, "POST", $request, $headers); // Throw Exception 468 | $this->restAPI->storeToProperties(); 469 | $this->restAPI->logout(); 470 | } 471 | } 472 | 473 | /** 474 | * Get the metadata information of the layout. Until ver.16 this method was 'getMetadata'. 475 | * @return object|null The metadata information of the layout. 476 | * It has just 1 property 'metaData' the array of the field information is set under the 'metaData' property. 477 | * There is no information about portals. Ex.: 478 | * {"metaData": [{"name": "id","type": "normal","result": "number","global": "false","repetitions": 1,"id": "1"}, 479 | *{"name": "name","type": "normal","result": "text","global": "false","repetitions": 1,"id": "2"},....,]} 480 | * @throws Exception In case of any error, an exception arises. 481 | */ 482 | public function getMetadataOld(): object|null 483 | { 484 | $returnValue = null; 485 | if ($this->restAPI->login()) { 486 | $request = []; 487 | $headers = ["Content-Type" => "application/json"]; 488 | $params = ['layouts' => $this->layout, 'metadata' => null]; 489 | $this->restAPI->callRestAPI($params, true, 'GET', $request, $headers); // Throw Exception 490 | $result = $this->restAPI->responseBody; 491 | $this->restAPI->storeToProperties(); 492 | $this->restAPI->logout(); 493 | $returnValue = $result->response; 494 | } 495 | return $returnValue; 496 | } 497 | 498 | /** 499 | * Get metadata information of the layout. 500 | * @return object|null The metadata information of the layout. 501 | * It has 3 properties 'fieldMetaData', 'portalMetaData' and 'valueLists'. 502 | * The later one has properties having portal object name of TO name. 503 | * The array of the field information is set under 'fieldMetaData' and the portal named properties. 504 | * Ex.: {"fieldMetaData": [{"name": "id","type": "normal","displayType": "editText","result": "number","global": false, 505 | * "autoEnter": true,"fourDigitYear": false,"maxRepeat": 1,"maxCharacters": 0,"notEmpty": false,"numeric": false, 506 | * "timeOfDay": false,"repetitionStart": 1,"repetitionEnd": 1},....,],"portalMetaData": {"Contact": [{ 507 | * "name": "contact_to::id","type": "normal",...},...], "history_to": [{"name": "history_to::id","type": "normal", 508 | * ...}...]} 509 | * @throws Exception In case of any error, an exception arises. 510 | */ 511 | public function getMetadata(): object|null 512 | { 513 | $returnValue = null; 514 | if ($this->restAPI->login()) { 515 | $request = []; 516 | $headers = ["Content-Type" => "application/json"]; 517 | $params = ['layouts' => $this->layout]; 518 | $this->restAPI->callRestAPI($params, true, 'GET', $request, $headers); // Throw Exception 519 | $result = $this->restAPI->responseBody; 520 | $this->restAPI->storeToProperties(); 521 | $this->restAPI->logout(); 522 | $returnValue = $result->response; 523 | } 524 | return $returnValue; 525 | } 526 | 527 | /** 528 | * Get debug information includes internal request URL and request body. 529 | * @return string 530 | */ 531 | public function getDebugInfo(): string 532 | { 533 | return $this->restAPI->url . " " . json_encode($this->restAPI->requestBody); 534 | } 535 | 536 | /** 537 | * Get the script error code. 538 | * @return int|null The value of the error code. 539 | * If any script wasn't called, returns null. 540 | */ 541 | public function getScriptError(): int|null 542 | { 543 | return $this->restAPI->scriptError; 544 | } 545 | 546 | /** 547 | * Get the return value from the script. 548 | * @return string|null The return value from the script. 549 | * If any script wasn't called, returns null. 550 | */ 551 | public function getScriptResult(): string|null 552 | { 553 | return $this->restAPI->scriptResult; 554 | } 555 | 556 | /** 557 | * Get the prerequest script error code. 558 | * @return int|null The value of the error code. 559 | * If any script wasn't called, returns null. 560 | */ 561 | public function getScriptErrorPrerequest(): int|null 562 | { 563 | return $this->restAPI->scriptErrorPrerequest; 564 | } 565 | 566 | /** 567 | * Get the return value from the prerequest script. 568 | * @return string|null The return value from the prerequest script. 569 | * If any script wasn't called, returns null. 570 | */ 571 | public function getScriptResultPrerequest(): string|null 572 | { 573 | return $this->restAPI->scriptResultPrerequest; 574 | } 575 | 576 | /** 577 | * Get the presort script error code. 578 | * @return int|null The value of the error code. 579 | * If any script wasn't called, returns null. 580 | */ 581 | public function getScriptErrorPresort(): int|null 582 | { 583 | return $this->restAPI->scriptErrorPresort; 584 | } 585 | 586 | /** 587 | * Get the return value from the presorted script. 588 | * @return string|null The return value from the presorted script. 589 | * If any script wasn't called, returns null. 590 | */ 591 | public function getScriptResultPresort(): string|null 592 | { 593 | return $this->restAPI->scriptResultPresort; 594 | } 595 | 596 | } 597 | -------------------------------------------------------------------------------- /src/Supporting/FileMakerRelation.php: -------------------------------------------------------------------------------- 1 | > The field value named as the property name. 16 | * @property FileMakerRelation $<> FileMakerRelation object associated with the property name. 17 | * The table occurrence name of the portal can be the 'portal_name,' and also the object name of the portal. 18 | * @version 33 19 | * @author Masayuki Nii 20 | * @copyright 2017-2024 Masayuki Nii (Claris FileMaker is registered trademarks of Claris International Inc. in the U.S. and other countries.) 21 | */ 22 | class FileMakerRelation implements Iterator 23 | { 24 | /** 25 | * @var null|array|object 26 | * @ignore 27 | */ 28 | private null|array|object $data; 29 | /** 30 | * @var object|array|null 31 | * @ignore 32 | */ 33 | private object|array|null $dataInfo; 34 | /** 35 | * @var null|string 36 | * @ignore 37 | */ 38 | private string|null $result; // OK for output from API, RECORD, PORTAL, PORTALRECORD 39 | /** 40 | * @var int|null 41 | * @ignore 42 | */ 43 | private int|null $errorCode; 44 | /** 45 | * @var int 46 | * @ignore 47 | */ 48 | private int $pointer = 0; 49 | /** 50 | * @var string|null 51 | * @ignore 52 | */ 53 | private string|null $portalName; 54 | /** 55 | * @var null|CommunicationProvider The instance of the communication class. 56 | * @ignore 57 | */ 58 | private ?CommunicationProvider $restAPI; 59 | 60 | /** 61 | * FileMakerRelation constructor. 62 | * 63 | * @param array $responseData 64 | * @param object|array $infoData 65 | * @param string $result 66 | * @param int $errorCode 67 | * @param string|null $portalName 68 | * @param CommunicationProvider|null $provider 69 | * 70 | * @ignore 71 | */ 72 | public function __construct(array|object $responseData, 73 | object|array $infoData, 74 | string $result = "PORTAL", 75 | int $errorCode = 0, 76 | string|null $portalName = null, 77 | ?CommunicationProvider $provider = null) 78 | { 79 | $this->data = $responseData; 80 | $this->dataInfo = $infoData; 81 | $this->result = $result; 82 | $this->errorCode = $errorCode; 83 | $this->portalName = $portalName; 84 | $this->restAPI = $provider; 85 | if ($errorCode === 0 && $portalName && is_array($infoData)) { 86 | foreach ($infoData as $pdItem) { 87 | if (property_exists($pdItem, 'portalObjectName') && $pdItem->portalObjectName == $portalName || 88 | !property_exists($pdItem, 'portalObjectName') && $pdItem->table == $portalName) { 89 | $this->dataInfo = $pdItem; 90 | } 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * @ignore 97 | */ 98 | public function getDataInfo(): object|array|null 99 | { 100 | return $this->dataInfo; 101 | } 102 | 103 | /** 104 | * Get the table occurrence name of a query to get this relation. 105 | * 106 | * @return null|string The table occurrence name. 107 | */ 108 | public function getTargetTable(): null|string 109 | { 110 | return $this->dataInfo->table ?? null; 111 | } 112 | 113 | /** 114 | * Get the total record count of a query to get this relation. 115 | * Portal relation doesn't have this information and returns NULL. 116 | * 117 | * @return null|int The total record count. 118 | */ 119 | public function getTotalCount(): null|int 120 | { 121 | return $this->dataInfo->totalRecordCount ?? null; 122 | } 123 | 124 | /** 125 | * Get the founded record count of a query to get this relation. 126 | * If the relation comes from getRecord() method, 127 | * this method returns 1. 128 | * 129 | * @return null|int The founded record count. 130 | */ 131 | public function getFoundCount(): null|int 132 | { 133 | return $this->dataInfo->foundCount ?? null; 134 | } 135 | 136 | /** 137 | * Get the returned record count of a query to get this relation. 138 | * If the relation comes from getRecord() method, 139 | * this method returns 1. 140 | * 141 | * @return null|int The returned record count. 142 | */ 143 | public function getReturnedCount(): null|int 144 | { 145 | return $this->dataInfo->returnedCount ?? null; 146 | } 147 | 148 | /** 149 | * If the portal name is different with the name used as the portal referencing name, this method can set it. 150 | * 151 | * @param string $name The portal name. 152 | */ 153 | public function setPortalName(string $name): void 154 | { 155 | $this->portalName = $name; 156 | } 157 | 158 | /** 159 | * The record pointer goes back to the previous record. This does not care the range of pointer value. 160 | */ 161 | public function previous(): void 162 | { 163 | $this->pointer--; 164 | } 165 | 166 | /** 167 | * The record pointer goes forward to the previous record. This does not care the range of pointer value. 168 | */ 169 | public function next(): void 170 | { 171 | $this->pointer++; 172 | } 173 | 174 | /** 175 | * The record pointer goes to the first record. 176 | */ 177 | public function last(): void 178 | { 179 | $this->pointer = count($this->data) - 1; 180 | } 181 | 182 | /** 183 | * The record pointer goes to the specified record. 184 | * 185 | * @param int $position The position of the record. The first record is 0. 186 | */ 187 | public function moveTo(int $position): void 188 | { 189 | $this->pointer = $position - 1; 190 | } 191 | 192 | /** 193 | * Count the number of records. 194 | * This method is defined in the Iterator interface. 195 | * 196 | * @return int The number of records. 197 | */ 198 | public function count(): int 199 | { 200 | return match ($this->result) { 201 | "OK", "PORTAL" => count($this->data), 202 | "RECORD", "PORTALRECORD" => 1, 203 | default => 0, 204 | }; 205 | } 206 | 207 | /** 208 | * @param $key 209 | * 210 | * @return FileMakerRelation|string|null 211 | * @throws Exception 212 | * @ignore 213 | */ 214 | public function __get($key) 215 | { 216 | return $this->field($key); 217 | } 218 | 219 | /** 220 | * Return the array of field names. 221 | * 222 | * @return array List of field names 223 | */ 224 | public function getFieldNames(): array 225 | { 226 | $list = []; 227 | if (isset($this->data)) { 228 | switch ($this->result) { 229 | case 'OK': 230 | if (isset($this->data[$this->pointer]->fieldData)) { 231 | foreach ($this->data[$this->pointer]->fieldData as $key => $val) { 232 | $list[] = $key; 233 | } 234 | } 235 | break; 236 | case 'PORTAL': 237 | if (isset($this->data[$this->pointer])) { 238 | foreach ($this->data[$this->pointer] as $key => $val) { 239 | $list[] = $key; 240 | } 241 | } 242 | break; 243 | case 'RECORD': 244 | if (isset($this->data->fieldData)) { 245 | foreach ($this->data->fieldData as $key => $val) { 246 | $list[] = $key; 247 | } 248 | } 249 | break; 250 | default: 251 | } 252 | } 253 | 254 | return $list; 255 | } 256 | 257 | /** 258 | * @param int $num 259 | * @return FileMakerRelation|null 260 | * @ignore 261 | */ 262 | private function getNumberedRecord(int $num): ?FileMakerRelation 263 | { 264 | $value = null; 265 | if (isset($this->data[$num])) { 266 | $tmpInfo = $this->getDataInfo(); 267 | $dataInfo = null; 268 | if (is_object($tmpInfo)) { 269 | $dataInfo = clone $tmpInfo; 270 | $dataInfo->returnedCount = 1; 271 | } 272 | $value = new FileMakerRelation( 273 | $this->data[$num], $dataInfo, ($this->result == "PORTAL") ? "PORTALRECORD" : "RECORD", 274 | $this->errorCode, $this->portalName, $this->restAPI); 275 | } 276 | return $value; 277 | } 278 | 279 | /** 280 | * Returns the first record of the query result. 281 | * 282 | * @return FileMakerRelation|null The record set of the record. 283 | */ 284 | public function getFirstRecord(): ?FileMakerRelation 285 | { 286 | return $this->getNumberedRecord(0); 287 | } 288 | 289 | /** 290 | * Returns the last record of the query result. 291 | * 292 | * @return FileMakerRelation|null The record set of the record. 293 | */ 294 | public function getLastRecord(): ?FileMakerRelation 295 | { 296 | return $this->getNumberedRecord(count($this->data) - 1); 297 | } 298 | 299 | /** 300 | * Returns the array of the query result. Usually iterating by using foreach is a better way. 301 | * 302 | * @return array The FileMakerRelation objects of the records. 303 | */ 304 | public function getRecords(): array 305 | { 306 | $records = []; 307 | foreach ($this as $record) { 308 | $records[] = $record; 309 | } 310 | return $records; 311 | } 312 | 313 | /** 314 | * Export to array 315 | * 316 | * @return array 317 | */ 318 | public function toArray(): array 319 | { 320 | switch ($this->result) { 321 | case 'OK': 322 | case 'PORTAL': 323 | $resultArray = []; 324 | foreach ($this as $record) { 325 | $resultArray[] = $record->toArray(); 326 | } 327 | return json_decode(json_encode($resultArray), true); 328 | case 'PORTALRECORD': 329 | if (isset($this->data)) { 330 | return json_decode(json_encode($this->data), true); 331 | } 332 | break; 333 | case 'RECORD': 334 | if (isset($this->data->fieldData)) { 335 | return json_decode(json_encode($this->data->fieldData), true); 336 | } 337 | break; 338 | } 339 | return []; 340 | } 341 | 342 | /** 343 | * Return the array of portal names. 344 | * 345 | * @return array List of portal names 346 | */ 347 | public function getPortalNames(): array 348 | { 349 | $list = []; 350 | if (isset($this->data)) { 351 | switch ($this->result) { 352 | case 'OK': 353 | foreach ($this->data as $key) { 354 | if (property_exists($key, 'portalData')) { 355 | foreach ($key->portalData as $name => $val) { 356 | $list[] = $name; 357 | } 358 | break 2; 359 | } 360 | } 361 | break; 362 | case 'RECORD': 363 | if (property_exists($this->data, 'portalData')) { 364 | foreach ($this->data->portalData as $name => $val) { 365 | $list[] = $name; 366 | } 367 | } 368 | } 369 | } 370 | return $list; 371 | } 372 | 373 | /** 374 | * The field value of the first parameter. 375 | * Or the FileMakerRelation object associated with the first parameter. 376 | * 377 | * @param string $name The field or portal name. 378 | * The table occurrence name of the portal can be the portal name, and also the object name of the portal. 379 | * @param string|null $toName The table occurrence name of the portal as the prefix of the field name. 380 | * 381 | * @return string|FileMakerRelation The field value as string, or the FileMakerRelation object of the portal. 382 | * @throws Exception The field specified in parameters doesn't exist. 383 | * @see FMDataAPI::setFieldHTMLEncoding() Compatible mode for FileMaker API for PHP. 384 | * 385 | */ 386 | public function field(string $name, string|null $toName = null): string|FileMakerRelation 387 | { 388 | $toName = is_null($toName) ? "" : "{$toName}::"; 389 | $fieldName = "{$toName}$name"; 390 | $value = null; 391 | if (isset($this->data)) { 392 | switch ($this->result) { 393 | case "OK": 394 | if (isset($this->data[$this->pointer])) { 395 | if (isset($this->data[$this->pointer]->fieldData->$name) 396 | ) { 397 | $value = $this->data[$this->pointer]->fieldData->$name; 398 | } elseif (isset($this->data[$this->pointer]->portalData->$name) 399 | ) { 400 | $infoData = property_exists($this->data[$this->pointer], 'portalDataInfo') ? 401 | $this->data[$this->pointer]->portalDataInfo : null; 402 | $value = new FileMakerRelation($this->data[$this->pointer]->portalData->$name, 403 | $infoData, "PORTAL", 0, $name, $this->restAPI); 404 | } 405 | } 406 | break; 407 | case "PORTAL": 408 | if (isset($this->data[$this->pointer]->$fieldName) 409 | ) { 410 | $value = $this->data[$this->pointer]->$fieldName; 411 | } 412 | break; 413 | case "RECORD": 414 | if (isset($this->data->fieldData->$name)) { 415 | $value = $this->data->fieldData->$name; 416 | } elseif (isset($this->data->portalData->$name)) { 417 | $infoData = property_exists($this->data, 'portalDataInfo') ? $this->data->portalDataInfo : null; 418 | $value = new FileMakerRelation($this->data->portalData->$name, $infoData, 419 | "PORTAL", 0, $name, $this->restAPI); 420 | } elseif (isset($this->data->fieldData->$fieldName)) { 421 | $value = $this->data->fieldData->$fieldName; 422 | } 423 | break; 424 | case "PORTALRECORD": 425 | $convinedName = "{$this->portalName}::{$fieldName}"; 426 | if (isset($this->data->$fieldName)) { 427 | $value = $this->data->$fieldName; 428 | } elseif (isset($this->data->$convinedName)) { 429 | $value = $this->data->$convinedName; 430 | } 431 | break; 432 | default: 433 | } 434 | } 435 | if (is_null($value)) { 436 | throw new Exception("Field {$fieldName} doesn't exist."); 437 | } 438 | if ($this->restAPI && $this->restAPI->fieldHTMLEncoding && !is_object($value)) { 439 | $value = htmlspecialchars($value); 440 | } 441 | return $value; 442 | } 443 | 444 | /** 445 | * Return the value of special field recordId in the current pointing record. 446 | * 447 | * @return int|null The value of special field recordId. 448 | */ 449 | public function getRecordId(): int|null 450 | { 451 | $value = null; 452 | switch ($this->result) { 453 | case "OK": 454 | if (isset($this->data[$this->pointer])) { 455 | if (isset($this->data[$this->pointer]->recordId) 456 | ) { 457 | $value = $this->data[$this->pointer]->recordId; 458 | } 459 | } 460 | break; 461 | case "PORTAL": 462 | if (isset($this->data[$this->pointer]->recordId) 463 | ) { 464 | $value = $this->data[$this->pointer]->recordId; 465 | } 466 | break; 467 | case "RECORD": 468 | case "PORTALRECORD": 469 | if (isset($this->data->recordId)) { 470 | $value = $this->data->recordId; 471 | } 472 | break; 473 | } 474 | 475 | return $value; 476 | } 477 | 478 | /** 479 | * Return the value of special field modId in the current pointing record. 480 | * 481 | * @return int The value of special field modId. 482 | */ 483 | public function getModId(): int 484 | { 485 | $value = null; 486 | switch ($this->result) { 487 | case "OK": 488 | if (isset($this->data[$this->pointer])) { 489 | if (isset($this->data[$this->pointer]->modId) 490 | ) { 491 | $value = $this->data[$this->pointer]->modId; 492 | } 493 | } 494 | break; 495 | case "PORTAL": 496 | if (isset($this->data[$this->pointer]->modId) 497 | ) { 498 | $value = $this->data[$this->pointer]->modId; 499 | } 500 | break; 501 | case "RECORD": 502 | case "PORTALRECORD": 503 | if (isset($this->data->modId)) { 504 | $value = $this->data->modId; 505 | } 506 | break; 507 | } 508 | 509 | return $value; 510 | } 511 | 512 | /** 513 | * Return the base64 encoded data in container field with streaming interface. The access with 514 | * streaming url depends on the setCertValidating(_) call, and it can work on self-signed certificate as a default. 515 | * Thanks to 'base64bits' as https://github.com/msyk/FMDataAPI/issues/18. 516 | * 517 | * @param string $name The container field name. 518 | * The table occurrence name of the portal can be the portal name, and also the object name of the portal. 519 | * @param string|null $toName The table occurrence name of the portal as the prefix of the field name. 520 | * 521 | * @return string|null The base64 encoded data in container field. 522 | * @throws Exception 523 | */ 524 | public function getContainerData(string $name, string|null $toName = null): string|null 525 | { 526 | $fieldValue = $this->field($name, $toName); 527 | if (!str_starts_with($fieldValue, "https://")) { 528 | throw new Exception("The field '{$name}' is not field name or container field."); 529 | } 530 | return $this->restAPI->accessToContainer($fieldValue); 531 | } 532 | 533 | /** 534 | * Return the current element. This method is implemented for Iterator interface. 535 | * 536 | * @return FileMakerRelation|null The record set of the current pointing record. 537 | */ 538 | public function current(): ?FileMakerRelation 539 | { 540 | $value = null; 541 | switch ($this->result) { 542 | case "OK": 543 | case "PORTAL": 544 | if (isset($this->data[$this->pointer])) { 545 | $tmpInfo = $this->getDataInfo(); 546 | $dataInfo = null; 547 | if (is_object($tmpInfo)) { 548 | $dataInfo = clone $tmpInfo; 549 | $dataInfo->returnedCount = 1; 550 | } 551 | $result = ($this->result == "PORTAL") ? "PORTALRECORD" : "RECORD"; 552 | $portalName = $this->portalName; 553 | $value = new FileMakerRelation($this->data[$this->pointer], $dataInfo, $result, 554 | $this->errorCode, $portalName, $this->restAPI); 555 | } 556 | break; 557 | case "RECORD": 558 | case "PORTALRECORD": 559 | $value = $this; 560 | break; 561 | } 562 | return $value; 563 | } 564 | 565 | /** 566 | * Return the key of the current element. This method is implemented for Iterator interface. 567 | * 568 | * @return integer The current number as the record pointer. 569 | */ 570 | public function key(): int 571 | { 572 | return $this->pointer; 573 | } 574 | 575 | /** 576 | * Checks if current position is valid. This method is implemented for Iterator interface. 577 | * 578 | * @return bool Returns true on existing the record or false on not existing. 579 | */ 580 | public function valid(): bool 581 | { 582 | switch ($this->result) { 583 | case "OK": 584 | case "PORTAL": 585 | if (isset($this->data[$this->pointer])) { 586 | return true; 587 | } 588 | break; 589 | case "RECORD": 590 | case "PORTALRECORD": 591 | return $this->pointer == 0; 592 | } 593 | return false; 594 | } 595 | 596 | /** 597 | * Rewind the Iterator to the first element. This method is implemented for Iterator interface. 598 | */ 599 | public function rewind(): void 600 | { 601 | $this->pointer = 0; 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /test/.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_SingleRecord":8,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_Query":7,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_ErrorQuery":7,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_initializeObjects":8},"times":{"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_initializeObjects":0.002,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_Query":0.001,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_ErrorQuery":0,"INTERMediator\\FileMakerServer\\RESTAPI\\FMDataAPIUnitTest::test_SingleRecord":0}} -------------------------------------------------------------------------------- /test/FMDataAPIUnitTest.php: -------------------------------------------------------------------------------- 1 | fmdataapi = new FMDataAPI("TestDB", "web", null, 20 | "localhost", "443", "https", null, true); 21 | } 22 | 23 | public function test_initializeObjects() 24 | { 25 | $this->assertNotNull($this->fmdataapi, 'FMDataAPI class must be instanticate.'); 26 | $this->assertEquals($this->fmdataapi->errorCode(), -1, 'It must be no error before calling.'); 27 | $this->assertEquals($this->fmdataapi->errorMessage(), "", 'It must be no error before calling.'); 28 | $this->assertEquals($this->fmdataapi->httpStatus(), 0, 'It must be no status before calling.'); 29 | } 30 | 31 | public function test_Query() 32 | { 33 | $result = $this->fmdataapi->person_layout->query(); 34 | $this->assertNotNull($result, 'Returned something.'); 35 | $this->assertEquals($result->count(), 3, 'Checking the record number.'); 36 | $this->assertEquals($result->getTargetTable(), 'person_to', 'Checking the table occurrence name.'); 37 | $this->assertEquals($result->getTotalCount(), 3, 'Checking the total record number.'); 38 | $this->assertEquals($result->getFoundCount(), 3, 'Checking the found record number.'); 39 | $this->assertEquals($result->getReturnedCount(), 3, 'Checking the returned record number.'); 40 | $this->assertEquals($result->getPortalNames(), ['Contact', 'History'], 'The query result returns portal names.'); 41 | 42 | $counter = 0; 43 | foreach ($result as $record) { 44 | $contacts = $record->Contact; 45 | if ($counter === 0) { 46 | $this->assertEquals($record->id, 1, 'Field value has to match with defined value.'); 47 | $this->assertEquals($record->name, 'Masayuki Nii', 'Field value has to match with defined value.'); 48 | $this->assertEquals($record->mail, 'msyk@msyk.net', 'Field value has to match with defined value.'); 49 | 50 | $this->assertEquals($record->getModId(), 6, 'It has ModID.'); 51 | $this->assertEquals($record->getRecordId(), 1, 'It has RecordID.'); 52 | $pcounter = 0; 53 | $this->assertEquals($contacts->count(), 3, 'Checking the record number.'); 54 | $this->assertEquals($contacts->getTargetTable(), 'contact_to', 'Checking the table occurrence name.'); 55 | $this->assertNull($contacts->getTotalCount(), 'Checking NULL as the total record number.'); 56 | $this->assertEquals($contacts->getFoundCount(), 3, 'Checking the found record number.'); 57 | $this->assertEquals($contacts->getReturnedCount(), 3, 'Checking the returned record number.'); 58 | 59 | foreach ($contacts as $item) { 60 | $item->setPortalName("contact_to"); 61 | if ($pcounter === 0) { 62 | $this->assertEquals($item->field("datetime"), '12/01/2009 15:23:00', 'Portal field value has to match with defined value.'); 63 | } else if ($pcounter === 1) { 64 | $this->assertEquals($item->field("datetime"), '12/02/2009 15:23:00', 'Portal field value has to match with defined value.'); 65 | } else if ($pcounter === 2) { 66 | $this->assertEquals($item->field("datetime"), '12/03/2009 15:23:00', 'Portal field value has to match with defined value.'); 67 | } 68 | $pcounter += 1; 69 | } 70 | $this->assertEquals($pcounter, 3, 'Cheking the record number in portal.'); 71 | } else if ($counter === 1) { 72 | $this->assertEquals($record->id, 2, ''); 73 | $this->assertEquals($record->name, 'Someone', 'Field value has to match with defined value.'); 74 | $this->assertEquals($record->mail, 'msyk@msyk.net', 'Field value has to match with defined value.'); 75 | $pcounter = 0; 76 | $this->assertEquals($contacts->count(), 2, 'Checking the record number.'); 77 | foreach ($contacts as $item) { 78 | if ($pcounter === 0) { 79 | $this->assertEquals($item->field("datetime", "contact_to"), '12/04/2009 15:23:00', 'Portal field value has to match with defined value.'); 80 | } else if ($pcounter === 1) { 81 | $this->assertEquals($item->field("datetime", "contact_to"), '12/01/2009 15:23:00', 'Portal field value has to match with defined value.'); 82 | } 83 | $pcounter += 1; 84 | } 85 | $this->assertEquals($pcounter, 2, 'Cheking the record number in portal.'); 86 | } else if ($counter === 2) { 87 | $this->assertEquals($record->id, 3, 'Field value has to match with defined value.'); 88 | $this->assertEquals($record->name, 'Anyone', 'Field value has to match with defined value.'); 89 | $this->assertEquals($record->mail, 'msyk@msyk.net', 'Field value has to match with defined value.'); 90 | $pcounter = 0; 91 | $this->assertEquals($contacts->count(), 2, 'Checking the record number.'); 92 | foreach ($contacts as $item) { 93 | if ($pcounter === 0) { 94 | $this->assertEquals($item->field("datetime", "contact_to"), '12/02/2009 15:23:00', 'Portal field value has to match with defined value.'); 95 | } else if ($pcounter === 1) { 96 | $this->assertEquals($item->field("datetime", "contact_to"), '12/03/2009 15:23:00', 'Portal field value has to match with defined value.'); 97 | } 98 | $pcounter += 1; 99 | } 100 | $this->assertEquals($pcounter, 2, 'Cheking the record number in portal.'); 101 | 102 | $recId = $record->getRecordId(); 103 | $this->assertEquals($recId, 333, 'The record id of last record must be 333.'); 104 | } 105 | $counter += 1; 106 | } 107 | } 108 | 109 | public function test_ErrorQuery() 110 | { 111 | $fm = new FMDataAPI("TestDB", "web", null, "localserver123", 112 | "443", "https", null, true); 113 | $result = $fm->person_layout->query(); // Host name is DNS unaware. 114 | $this->assertNull($result, 'No results returns.'); 115 | $this->assertEquals($fm->httpStatus(), 0, 'Returns 0 for http status.'); 116 | $this->assertEquals($fm->errorCode(), -1, 'The error code has to be -1.'); 117 | // $this->assertEquals($fm->curlErrorCode(), 6, 'The error code has to be 6.'); 118 | } 119 | 120 | public function test_SingleRecord() 121 | { 122 | $result = $this->fmdataapi->person_layout->query(); 123 | $record = $result->getFirstRecord(); 124 | $this->assertEquals($record->id, 1, 'Field value has to match with defined value.'); 125 | $this->assertEquals($record->name, 'Masayuki Nii', 'Field value has to match with defined value.'); 126 | $this->assertEquals($record->mail, 'msyk@msyk.net', 'Field value has to match with defined value.'); 127 | $pcounter = 0; 128 | $contacts = $record->Contact; 129 | $this->assertEquals($contacts->count(), 3, 'Checking the record number.'); 130 | $this->assertEquals($contacts->getTargetTable(), 'contact_to', 'Checking the table occurrence name.'); 131 | $this->assertNull($contacts->getTotalCount(), 'Checking NULL as the total record number.'); 132 | $this->assertEquals($contacts->getFoundCount(), 3, 'Checking the found record number.'); 133 | $this->assertEquals($contacts->getReturnedCount(), 3, 'Checking the returned record number.'); 134 | 135 | foreach ($contacts as $item) { 136 | if ($pcounter === 0) { 137 | $this->assertEquals($item->field("datetime", "contact_to"), '12/01/2009 15:23:00', 'Portal field value has to match with defined value.'); 138 | } else if ($pcounter === 1) { 139 | $this->assertEquals($item->field("datetime", "contact_to"), '12/02/2009 15:23:00', 'Portal field value has to match with defined value.'); 140 | } else if ($pcounter === 2) { 141 | $this->assertEquals($item->field("datetime", "contact_to"), '12/03/2009 15:23:00', 'Portal field value has to match with defined value.'); 142 | } 143 | $pcounter += 1; 144 | } 145 | $this->assertEquals($pcounter, 3, 'Cheking the record number in portal.'); 146 | 147 | $this->assertEquals($record->count(), 1, 'The single record is just one record.'); 148 | $currentRecord = $record->current(); 149 | $this->assertEquals($currentRecord->id, 1, 'The single record can call current and return a Relation.'); 150 | $this->assertEquals($record->getPortalNames(), ['Contact', 'History'], 'The single record returns portal names.'); 151 | $this->assertEquals($record->getModId(), 6, 'The single record returns ModID.'); 152 | $this->assertEquals($record->getRecordId(), 1, 'The single record returns RecordID.'); 153 | $this->assertEquals($record->getTargetTable(), 'person_to', 'The single record returns the target table.'); 154 | 155 | $pcount = 0; 156 | foreach ($record as $item) { 157 | $this->assertEquals($item->id, 1, 'The single record can iterate.'); 158 | $pcount++; 159 | } 160 | $this->assertEquals($pcount, 1, 'The single record has to repeat just once.'); 161 | $this->assertEquals($record->getTotalCount(), 3,'Checking the total record number for queried data.'); 162 | $this->assertEquals($record->getFoundCount(), 3, 'Checking the found record number for queried data.'); 163 | $this->assertEquals($record->getReturnedCount(), 1, 'Checking the returned record number.'); 164 | 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /test/HashForTestInput.php: -------------------------------------------------------------------------------- 1 | "https://localhost:443/fmi/data/vLatest/databases/TestDB/sessions", 12 | 'method' => 'post', 13 | 'header' => [0 => 'Content-Type: application/json', 1 => 'Authorization: Basic d2ViOnBhc3N3b3Jk'], 14 | 'request' => ['fmDataSource' => true] 15 | ] 16 | )), "\n"; 17 | 18 | echo sha1(json_encode( 19 | [ //Logout 20 | 'url' => "https://localhost:443/fmi/data/vLatest/databases/TestDB/sessions/1f3c9bd128ef29e97b2d7fd941df4a88198bd8b5eb9aa69c4", 21 | 'method' => 'get', 22 | 'header' => ['Authorization: Bearer 1f3c9bd128ef29e97b2d7fd941df4a88198bd8b5eb9aa69c4'], 23 | 'request' => null 24 | ] 25 | )), "\n"; 26 | 27 | echo sha1(json_encode( 28 | [ // Simple Query 29 | 'url' => "https://localhost:443/fmi/data/vLatest/databases/TestDB/layouts/person_layout/records", 30 | 'method' => 'get', 31 | 'header' => ['Authorization: Bearer 1f3c9bd128ef29e97b2d7fd941df4a88198bd8b5eb9aa69c4', 'Content-Type: application/json'], 32 | 'request' => [] 33 | ] 34 | )), "\n"; 35 | 36 | echo sha1(json_encode( 37 | [ // Error Simulation 38 | 'url' => "https://localhost:443/fmi/data/vLatest/databases/TestDB/layouts/person_layout2/records", 39 | 'method' => 'get', 40 | 'header' => ['Authorization: Bearer 1f3c9bd128ef29e97b2d7fd941df4a88198bd8b5eb9aa69c4', 'Content-Type: application/json'], 41 | 'request' => [] 42 | ] 43 | )), "\n"; 44 | 45 | echo sha1(json_encode( 46 | [ // Error Simulation - illegal host name 47 | 'url' => "https://localserver123:443/fmi/data/vLatest/databases/TestDB/sessions", 48 | 'method' => 'post', 49 | 'header' => [0 => 'Content-Type: application/json', 1 => 'Authorization: Basic d2ViOnBhc3N3b3Jk'], 50 | 'request' => ['fmDataSource' => false] 51 | ] 52 | )), "\n"; 53 | 54 | echo sha1(json_encode( 55 | [ // Error Simulation - Old version server 56 | 'url' => "https://10.0.1.21:443/fmi/data/vLatest/databases/TestDB/sessions", 57 | 'method' => 'post', 58 | 'header' => [0 => 'Content-Type: application/json', 1 => 'Authorization: Basic d2ViOnBhc3N3b3Jk'], 59 | 'request' => ['fmDataSource' => false] 60 | ] 61 | )), "\n"; 62 | 63 | -------------------------------------------------------------------------------- /test/TestProvider.php: -------------------------------------------------------------------------------- 1 | buildResponses(); 37 | } 38 | 39 | /** 40 | * Override communication method. 41 | * @param array $params 42 | * @param bool $isAddToken 43 | * @param string $method 44 | * @param array|null|string $request 45 | * @param array|null $addHeader 46 | * @param bool $isSystem for Metadata 47 | * @param string|null|false $directPath 48 | * @return void 49 | * @throws Exception In case of any error, an exception arises. 50 | * @ignore 51 | */ 52 | public function callRestAPI(array $params, 53 | bool $isAddToken, 54 | string $method = 'GET', 55 | array|null|string $request = null, 56 | array|null $addHeader = null, 57 | bool $isSystem = false, 58 | string|null|false $directPath = null): void 59 | { 60 | $methodLower = strtolower($method); 61 | $url = $this->getURL($params, $request, $methodLower); 62 | $header = $this->getHeaders($isAddToken, $addHeader); 63 | if ($methodLower !== 'get' && !is_null($request) && !is_string($request)) { 64 | $request = $this->justifyRequest($request); 65 | } 66 | $inputs = ['url' => $url, 'method' => $methodLower, 'header' => $header, 'request' => $request]; 67 | $response = $this->validResponse($inputs); 68 | 69 | $this->curlInfo = $response['curlinfo']; 70 | $this->curlErrorNumber = $response['curlerror'] ?? 0; 71 | $this->curlError = $response['curlerrormessage'] ?? ""; 72 | 73 | $this->method = $method; 74 | $this->url = $url; 75 | $this->requestHeader = $header; 76 | $this->requestBody = ($methodLower != 'get') ? $request : null; 77 | if ($response['response']) { 78 | $this->responseBody = json_decode($response['response'], false, 512, JSON_BIGINT_AS_STRING); 79 | } 80 | } 81 | 82 | /** 83 | * Override communication method. 84 | * @param string $url 85 | * @return string The base64 encoded data in container field. 86 | */ 87 | public function accessToContainer(string $url): string 88 | { 89 | return "TODO TestProvider::accessToContainer()"; 90 | } 91 | 92 | private function validResponse($input) 93 | { 94 | $hash = sha1(json_encode($input)); 95 | foreach ($this->responses as $key => $value) { 96 | if ($hash === $key) { 97 | return $value; 98 | } 99 | } 100 | } 101 | 102 | private $responses; 103 | 104 | private function buildResponses() 105 | { 106 | $this->responses = [ 107 | 'baebc873017a6d313d20a113ef506307b0c9f575' => [ //Login 108 | 'response' => '{"response":{"token":"1f3c9bd128ef29e97b2d7fd941df4a88198bd8b5eb9aa69c4"},"messages":[{"code":"0","message":"OK"}]}', 109 | 'curlerror' => '0', 110 | 'curlerrormessage' => '', 111 | 'curlinfo' => ['http_code' => 200] 112 | ], 113 | '5a836a66dee3facd0875bfee60a1a46f52edd0c3' => [ //LogOut 114 | 'response' => '{"response": {},"messages": [{"code": "0", "message": "OK"}]}', 115 | 'curlerror' => '0', 116 | 'curlerrormessage' => '', 117 | 'curlinfo' => ['http_code' => 200] 118 | ], 119 | '7e48895adbe2811ce7e51568542f2cf12779c710' => [ //LogOut 120 | 'response' => '{"response": {},"messages": [{"code": "0", "message": "OK"}]}', 121 | 'curlerror' => '0', 122 | 'curlerrormessage' => '', 123 | 'curlinfo' => ['http_code' => 200] 124 | ], 125 | 'd973a3dff40c30a307a9872714c88f9ee6873dd5' => [ //Simple Query 126 | 'response' => '{ 127 | "response": { 128 | "dataInfo": { 129 | "database": "TestDB", 130 | "layout": "person_layout", 131 | "table": "person_to", 132 | "totalRecordCount": 3, 133 | "foundCount": 3, 134 | "returnedCount": 3 135 | }, 136 | "data": [ 137 | { 138 | "fieldData": { 139 | "id": 1, 140 | "name": "Masayuki Nii", 141 | "address": "Saitama, Japan", 142 | "mail": "msyk@msyk.net", 143 | "category": 102, 144 | "checking": 1, 145 | "location": 201, 146 | "memo": "" 147 | }, 148 | "portalData": { 149 | "Contact": [ 150 | { 151 | "recordId": "1", 152 | "contact_to::id": 1, 153 | "contact_to::person_id": 1, 154 | "contact_to::summary": "Telephone", 155 | "contact_to::datetime": "12/01/2009 15:23:00", 156 | "contact_to::description": "a\rb", 157 | "contact_to::important": "", 158 | "contact_to::way": 4, 159 | "contact_to::kind": 4, 160 | "modId": "1" 161 | }, 162 | { 163 | "recordId": "2", 164 | "contact_to::id": 2, 165 | "contact_to::person_id": 1, 166 | "contact_to::summary": "Meetings", 167 | "contact_to::datetime": "12/02/2009 15:23:00", 168 | "contact_to::description": "aq", 169 | "contact_to::important": 1, 170 | "contact_to::way": 4, 171 | "contact_to::kind": 7, 172 | "modId": "3" 173 | }, 174 | { 175 | "recordId": "3", 176 | "contact_to::id": 3, 177 | "contact_to::person_id": 1, 178 | "contact_to::summary": "Mail", 179 | "contact_to::datetime": "12/03/2009 15:23:00", 180 | "contact_to::description": "", 181 | "contact_to::important": "", 182 | "contact_to::way": 5, 183 | "contact_to::kind": 8, 184 | "modId": "0" 185 | } 186 | ], 187 | "History": [ 188 | { 189 | "recordId": "1", 190 | "history_to::id": 1, 191 | "history_to::person_id": 1, 192 | "history_to::description": "Hight School", 193 | "history_to::startdate": "04/01/2001", 194 | "history_to::enddate": "03/31/2003", 195 | "history_to::username": "", 196 | "modId": "0" 197 | }, 198 | { 199 | "recordId": "2", 200 | "history_to::id": 2, 201 | "history_to::person_id": 1, 202 | "history_to::description": "University", 203 | "history_to::startdate": "04/01/2003", 204 | "history_to::enddate": "03/31/2007", 205 | "history_to::username": "", 206 | "modId": "0" 207 | } 208 | ] 209 | }, 210 | "recordId": "1", 211 | "modId": "6", 212 | "portalDataInfo": [ 213 | { 214 | "portalObjectName": "Contact", 215 | "database": "TestDB", 216 | "table": "contact_to", 217 | "foundCount": 3, 218 | "returnedCount": 3 219 | }, 220 | { 221 | "portalObjectName": "History", 222 | "database": "TestDB", 223 | "table": "history_to", 224 | "foundCount": 2, 225 | "returnedCount": 2 226 | } 227 | ] 228 | }, 229 | { 230 | "fieldData": { 231 | "id": 2, 232 | "name": "Someone", 233 | "address": "Tokyo, Japan", 234 | "mail": "msyk@msyk.net", 235 | "category": "", 236 | "checking": "", 237 | "location": "", 238 | "memo": "" 239 | }, 240 | "portalData": { 241 | "Contact": [ 242 | { 243 | "recordId": "4", 244 | "contact_to::id": 4, 245 | "contact_to::person_id": 2, 246 | "contact_to::summary": "Calling", 247 | "contact_to::datetime": "12/04/2009 15:23:00", 248 | "contact_to::description": "", 249 | "contact_to::important": "", 250 | "contact_to::way": 6, 251 | "contact_to::kind": 12, 252 | "modId": "0" 253 | }, 254 | { 255 | "recordId": "5", 256 | "contact_to::id": 5, 257 | "contact_to::person_id": 2, 258 | "contact_to::summary": "Telephone", 259 | "contact_to::datetime": "12/01/2009 15:23:00", 260 | "contact_to::description": "", 261 | "contact_to::important": "", 262 | "contact_to::way": 4, 263 | "contact_to::kind": 4, 264 | "modId": "0" 265 | } 266 | ], 267 | "History": [] 268 | }, 269 | "recordId": "2", 270 | "modId": "0", 271 | "portalDataInfo": [ 272 | { 273 | "portalObjectName": "Contact", 274 | "database": "TestDB", 275 | "table": "contact_to", 276 | "foundCount": 2, 277 | "returnedCount": 2 278 | }, 279 | { 280 | "portalObjectName": "History", 281 | "database": "TestDB", 282 | "table": "history_to", 283 | "foundCount": 0, 284 | "returnedCount": 0 285 | } 286 | ] 287 | }, 288 | { 289 | "fieldData": { 290 | "id": 3, 291 | "name": "Anyone", 292 | "address": "Osaka, Japan", 293 | "mail": "msyk@msyk.net", 294 | "category": 101, 295 | "checking": 1, 296 | "location": 202, 297 | "memo": "" 298 | }, 299 | "portalData": { 300 | "Contact": [ 301 | { 302 | "recordId": "6", 303 | "contact_to::id": 6, 304 | "contact_to::person_id": 3, 305 | "contact_to::summary": "Meeting", 306 | "contact_to::datetime": "12/02/2009 15:23:00", 307 | "contact_to::description": "", 308 | "contact_to::important": 1, 309 | "contact_to::way": 4, 310 | "contact_to::kind": 7, 311 | "modId": "0" 312 | }, 313 | { 314 | "recordId": "7", 315 | "contact_to::id": 7, 316 | "contact_to::person_id": 3, 317 | "contact_to::summary": "Mail etcsss", 318 | "contact_to::datetime": "12/03/2009 15:23:00", 319 | "contact_to::description": "aaaqq", 320 | "contact_to::important": "", 321 | "contact_to::way": 5, 322 | "contact_to::kind": 8, 323 | "modId": "4" 324 | } 325 | ], 326 | "History": [] 327 | }, 328 | "recordId": "333", 329 | "modId": "6", 330 | "portalDataInfo": [ 331 | { 332 | "portalObjectName": "Contact", 333 | "database": "TestDB", 334 | "table": "contact_to", 335 | "foundCount": 2, 336 | "returnedCount": 2 337 | }, 338 | { 339 | "portalObjectName": "History", 340 | "database": "TestDB", 341 | "table": "history_to", 342 | "foundCount": 0, 343 | "returnedCount": 0 344 | } 345 | ] 346 | } 347 | ] 348 | }, 349 | "messages": [ 350 | { 351 | "code": "0", 352 | "message": "OK" 353 | } 354 | ] 355 | }', 356 | 'curlerror' => '0', 357 | 'curlerrormessage' => '', 358 | 'curlinfo' => ['http_code' => 200] 359 | ], 360 | '2fd2ddb55e5f93862602b27185fe83d2f5e03e5c' => [ // Error Simulation 361 | 'response' => '{"messages":[{"code":"105","message":"Layout is missing"}],"response":{}}', 362 | 'curlerror' => '0', 363 | 'curlerrormessage' => '', 364 | 'curlinfo' => ['http_code' => 500] 365 | ], 366 | '9f4afb05c4cddcd7de774119976e9ab868633af1' => [// Error Simulation - illegal host name 367 | 'response' => null, 368 | 'curlerror' => 6, 369 | 'curlerrormessage' => 'Could not resolve host: localserver123', 370 | 'curlinfo' => ['http_code' => 0] 371 | ], 372 | '977673d8758a3ec9253cc7429259766bb60b8114' => [// Error Simulation - old version server 373 | 'response' => null, 374 | 'curlerror' => 0, 375 | 'curlerrormessage' => '', 376 | 'curlinfo' => ['http_code' => 404] 377 | ], 378 | 'hash' => [ 379 | 'response' => '', 380 | 'curlerror' => '0', 381 | 'curlerrormessage' => '', 382 | 'curlinfo' => ['http_code' => 200] 383 | ], 384 | ]; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /test/phpstan-baseline.neon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msyk/FMDataAPI/71032a349d629313d0138b276cd64bba716bcf25/test/phpstan-baseline.neon -------------------------------------------------------------------------------- /test/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | reportUnmatchedIgnoredErrors: false 6 | universalObjectCratesClasses: 7 | - INTERMediator\FileMakerServer\RESTAPI\FMDataAPI 8 | -------------------------------------------------------------------------------- /test/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------