├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO ├── VERSION ├── composer.json ├── data ├── README.md ├── bfpo_formats.yml ├── country_codes.yml ├── international_documents.yml ├── label_formats.yml ├── service_enhancements.yml ├── service_formats.yml ├── service_offerings.yml └── service_types.yml ├── examples └── BasicUsageTest.php ├── reference ├── CDM │ ├── CommonClassesV2_3.xsd │ ├── CommonIntegrationSchemaV1_11.xsd │ ├── CommonIntegrationSchemaV1_11_1.xsd │ ├── DatatypesV2_3.xsd │ ├── RMGSOAPExtensionsV1_2.xsd │ └── ReferenceDataV2_3.xsd ├── README.md ├── RMG_Shipping_API_(SOAP)_Technical_User_Guide_v2_4.pdf ├── SAPI-Reference-Data-v2-2_01-June-2015.xlsx ├── ShippingAPI_V2_0_9.wsdl ├── ShippingAPI_V2_0_9.xsd ├── oasis-200401-wss-wssecurity-secext-1.0.xsd ├── oasis-200401-wss-wssecurity-utility-1.0.xsd ├── responses │ ├── cancelShipmentErrorAndWarningResponse.xml │ ├── cancelShipmentMultipleErrorResponse.xml │ ├── cancelShipmentMultipleWarningResponse.xml │ ├── cancelShipmentResponse.xml │ ├── cancelShipmentSingleErrorResponse.xml │ ├── cancelShipmentSingleWarningResponse.xml │ ├── createManifestResponse.xml │ ├── createShipmentInternationalResponse.xml │ ├── createShipmentResponse.xml │ ├── printDocumentResponse.xml │ ├── printLabelResponse(International - Datastream with Images).xml │ ├── printLabelResponse(International with Localised Address).xml │ ├── printLabelResponse.xml │ ├── printManifestResponse.xml │ ├── request1DRangesResponse.xml │ ├── request2DItemIDRangeResponse.xml │ └── updateShipmentResponse.xml ├── xml.xsd └── xmldsig-core-schema.xsd ├── run_tests.sh ├── src ├── Connector │ ├── MockSoapClient.php │ ├── TDSoapClient.php │ ├── baseConnector.php │ └── soapConnector.php ├── Exception │ ├── RequestException.php │ ├── ResponseException.php │ ├── RoyalMailException.php │ ├── StructureSkipFieldException.php │ └── ValidatorException.php ├── Filter │ └── Filters.php ├── Helper │ ├── Data.php │ ├── Development.php │ ├── Form.php │ ├── Help.php │ └── Structure.php ├── Request │ ├── Builder.php │ └── schema │ │ ├── cancelShipment.yml │ │ ├── createManifest.yml │ │ ├── createShipment.yml │ │ ├── integrationHeader.yml │ │ ├── printDocument.yml │ │ ├── printLabel.yml │ │ ├── printManifest.yml │ │ ├── request1DRanges.yml │ │ └── request2DItemIDRange.yml ├── Response │ ├── Interpreter.php │ └── schema │ │ ├── cancelShipment.yml │ │ ├── createManifest.yml │ │ ├── createShipment.yml │ │ ├── integrationFooter.yml │ │ ├── integrationHeader.yml │ │ ├── printDocument.yml │ │ ├── printLabel.yml │ │ ├── printManifest.yml │ │ ├── request1DRanges.yml │ │ └── request2DItemIDRange.yml ├── RoyalMail.php └── Validator │ └── Validates.php └── tests ├── Bootstrap.php ├── README.md ├── lib └── TestDataLoader.php ├── onboarding └── README ├── resources ├── misc_builder_tests.yml ├── requests │ ├── cancelShipment.yml │ ├── createManifest.yml │ ├── createShipment.yml │ ├── createShipment_International.yml │ ├── integrationHeader.yml │ ├── printDocument.yml │ ├── printLabel.yml │ ├── printManifest.yml │ ├── request1DRanges.yml │ └── request2DItemIDRange.yml └── responses │ ├── cancelShipment.yml │ ├── createManifest.yml │ ├── createShipment.yml │ ├── printDocument.yml │ ├── printLabel.yml │ ├── printManifest.yml │ ├── request1DRanges.yml │ └── request2DItemIDRange.yml └── unit ├── Connector └── soapConnectorTest.php ├── Filter └── FilterTest.php ├── Helper ├── DataTest.php └── StructureTest.php ├── Request └── BuilderTest.php ├── Response └── InterpreterTest.php ├── RoyalMailTest.php └── Validator └── ValidatesTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | tests/tmp/* 3 | composer.lock 4 | *.sublime-project 5 | *.sublime-workspace 6 | .rules/* 7 | tmp.php 8 | tmp_* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | 8 | 9 | before_script: phpenv config-rm xdebug.ini 10 | 11 | install: composer install 12 | 13 | script: ./vendor/bin/atoum -bf tests/Bootstrap.php -d tests/unit/ 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### 0.0 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Turtle Design 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Royal Mail API SDK for PHP 2 | 3 | [![Build Status](https://travis-ci.org/turtledesign/royalmail-php.png?branch=master)](https://travis-ci.org/turtledesign/royalmail-php) 4 | 5 | This repository contains a PHP SDK/Interface for the (UK) [Royal Mail's Shipping API](http://www.royalmail.com/corporate/services/shipping-api) 6 | 7 | > This is not an official SDK, we (http://www.turtledesign.com/) are a 3rd party integrator releasing the module with an open source licence because why not. 8 | 9 | ## Prerequisites 10 | 11 | - PHP 5.4 or above (testing requires version 5.6 or above due to the Atoum test module requirements). 12 | - [soap](http://php.net/manual/en/book.soap.php) & [openssl](http://php.net/manual/en/book.openssl.php) extensions must be enabled 13 | - [fileinfo](http://php.net/manual/en/book.fileinfo.php) required for testing. 14 | 15 | ## Installation 16 | 17 | ### - Using Composer 18 | [**composer**](https://getcomposer.org/) is the recommended way to install the SDK. To use the SDK with project, add the following dependency to your application's composer.json and run `composer update --no-dev` to fetch the SDK. 19 | 20 | You can download composer using instructions on [Composer Official Website.](https://getcomposer.org/download/) 21 | 22 | #### Prerequisites 23 | - *composer* for fetching dependencies (See [http://getcomposer.org](http://getcomposer.org)) 24 | 25 | #### Steps to Install : 26 | 27 | Currently, the SDK is available at [https://packagist.org](https://packagist.org/packages/turtledesign/royalmail-php). To use it in your project, you need to include it as a dependency in your project composer.json file. It can be done in two ways : 28 | 29 | * Running `composer require 'turtledesign/royalmail-php:*@dev'` command on your project root location (where project composer.json is located.) 30 | 31 | * Or, manually editing composer.json file `require` field, and adding `"turtledesign/royalmail-php" : "*@dev"` inside it. 32 | 33 | The resultant sample *composer.json* would look like this: 34 | 35 | ```php 36 | { 37 | ... 38 | 39 | "name": "sample/website", 40 | "require": { 41 | "turtledesign/royalmail-php" : "*@dev" 42 | } 43 | 44 | ... 45 | } 46 | ``` 47 | 48 | ### - Direct Download (without using Composer) 49 | 50 | If you do not want to use composer, you can grab the SDK zip that contains Royal Mail API SDK with all its dependencies with it. 51 | 52 | #### Steps to Install : 53 | - Download zip archive with desired version from our [Releases](https://github.com/turtledesign/royalmail-php/releases). Each release will have a `direct-download-*.zip` that contains PHP Rest API SDK and its dependencies. 54 | 55 | - Unzip and copy vendor directory inside your project, e.g. project root directory. 56 | 57 | - If your application has a bootstrap/autoload file, you should add 58 | `include '/vendor/autoload.php'` in it. The location of the `` should be replaced based on where you downloaded **vendor** directory in your application. 59 | 60 | - This *autoload.php* file registers a custom autoloader that can autoload the Royal Mail SDK files, that allows you to access PHP SDK system in your application. 61 | 62 | 63 | 64 | ## More help 65 | 66 | * [Royal Mail Shipping API Service Page](http://www.royalmail.com/corporate/services/shipping-api) 67 | * [API Reference - PDF](http://www.royalmail.com/sites/default/files/Shipping-API-Technical-User-Guide-v2_1-June-2015.pdf) 68 | * [Reporting issues / feature requests](https://github.com/turtledesign/royalmail-php/issues) 69 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 2 | 3 | > May need to have all the xmlns files local (& update sample test/responses to use them). 4 | > Add in a filter or pre-process to allow snake_case for input array data rather than camelCaseAsPerRM. 5 | > Add code coverage analysis. 6 | > Add failure tests for validators and filters. 7 | > Move more fully to Valitron validators. 8 | > Better handling for multi-values in the interpreters - currently only handles them with nesting. 9 | > > Could do with separating it out as the handling is different in Builder and Interpreter and currently requires bypass to avoid conflict. 10 | > createShipment needs the multi-option values setting up. 11 | > Automatic error minimising way to check request conversion schema completeness - parse WSDL for all possible values? 12 | > May not need the success flag on responses - any failure should have an error response. 13 | > Catch SOAP error objects and pass them back in the same object format? or throw a standard (interface unrelated) error the interpreter can parse. 14 | > > Layout of items input could be tidied a lot - e.g. ~/items/ as root array with number of separate, weight doesn't need value subkey. 15 | > SOAP response verification/security. 16 | > printManifest request requires at least one value. 17 | > SOAP client can be intitialised with 'features' => SOAP_SINGLE_ELEMENT_ARRAYS -- may save some work. 18 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2 | 0.4.0.2 30/07/2018 https://github.com/turtledesign/royalmail-php/issues/12 - $helper in Interpreter() call should be [] not Helper/Data object. 3 | 0.4.0.1 30/06/2018 https://github.com/turtledesign/royalmail-php/issues/11 - Parameter count mismatch on new Interpreter() call. 4 | 0.4.0.0 16/03/2018 Added the international shipping options to the createShipment schema. 5 | 0.3.1.6 14/03/2018 Fixed createLabel ->succeeded() response and improved Symfony YAML requirement specification. 6 | 0.3.1.4 09/03/2018 Postal town shouldn't have been required on printLabel as the address is option in its entirity. 7 | 0.3.1.3 09/03/2018 Removed call to each() in validation module as it's deprecated in PHP 7.2 8 | 0.3.1.2 09/03/2018 Fixed errors in some test data files that YAML 4.* picks up where previous versions ignored. 9 | 0.3.1.1 09/03/2018 Added v4 of YAML package and latest Atoum for compatability with Laravel 5.6 and PHP 7.1 10 | 0.3.0.1 06/03/2018 Added some extra tests while hunting down why the weight value wasn't working. 11 | 0.3.0.0 18/10/2016 Updated to the latest version (v2_0_9) of the API + new endpoint as per https://developer.royalmail.net/node/688#/ 12 | 0.2.0.0 18/10/2016 Fix for PHP 7 (it doesn't like references in soap client arrays). 13 | 0.1.10.6 14/08/2016 Added basic JSON serialisation for the response/interpreter object. 14 | 0.1.9.6 06/01/2016 Added (and removed) PHP 7 to Travis CI test - errors on Travis setup. 15 | 0.1.9.5 06/01/2016 Modified the error and warning test sample responses to account for RM apparently removing xmlns files. [1] 16 | 0.1.9.4 30/12/2015 Merged in changes from @minioak - fixed up tests. [1.5] 17 | 0.1.9.0 16/10/2015 Added request1DRanges action. [0.6] 18 | 0.1.8.0 16/10/2015 Added an exception catcher to provide debug info in the response object. [1.89] 19 | 0.1.7.0 16/10/2015 Added printDocument action. [0.2] 20 | 0.1.6.0 16/10/2015 Added createManifest action & re-organised response test YML files. [0.7] 21 | 0.1.5.0 16/10/2015 createShipment response map. [1] 22 | 0.1.4.0 14/10/2015 Couple of changes to get all the auth active and usable. [1.5] 23 | 0.1.3.0 12/10/2015 Linked up dev helper, added basic usage example. [0.6] 24 | 0.1.2.0 11/10/2015 Extracted test schema loading to the (new) Development helper [0.8] 25 | 0.1.1.0 11/10/2015 Added magic API request action methods. [0.5] 26 | 0.1.0.0 11/10/2015 First Alpha Release - Main entry dispatcher working end to end in development mode. 27 | 0.0.25.1 11/10/2015 Work on dispatcher/entry point - Helper and Connector loading working, inc. static version for dev integration. [3] 28 | 0.0.24.1 04/10/2015 Added printLabel request + testing and methods for binary files. [3] 29 | 0.0.23.1 02/10/2015 serviceEnhancements working with subkey nesting of values. [0.5] 30 | 0.0.22.1 02/10/2015 Items key on createShipment working, added Round filter and generation of county key for multi-properties [2.5] 31 | 0.0.21.1 30/09/2015 Added loading for the errors and warnings in the integrationFooter, tested with single error [2.5 (+1c)] 32 | 0.0.20.1 27/09/2015 Got the createManifest response parsing OK, including handling multiple/single shipment values [4] 33 | 0.0.19.1 26/09/2015 Response interpreter working (with filters+meta) and tested with the provided cancelShipment response [3.9] 34 | 0.0.18.1 23/09/2015 Factored out a load of the structure processing to implement it in Request and Response + dev on response interpreter. [1.8] 35 | 0.0.17.1 23/09/2015 Got the WS security SOAP header functioning [1.7] 36 | 0.0.16.1 21/09/2015 Added mock soap client and tests [6.8] 37 | 0.0.15.1 20/09/2015 Basic multiple element addition [1.8] 38 | 0.0.14.1 15/09/2015 Added Address block with filter and validator for UK postcodes [2.4] 39 | 0.0.13.1 13/09/2015 Added conditional require: ThisRequiredWhenThat [2.2] 40 | 0.0.12.1 13/09/2015 Start switch to Valitron validation, implemented + tested SkipThisIfThatEmpty filtering for conditional value inclusion. [3.5] 41 | 0.0.11.1 11/09/2015 Skip filtering added + couple more string filters. Added help to schema. createShipment passing tests up to recipientContact. [3.5] 42 | 0.0.10.1 11/09/2015 Added tests for the data files and cleaned the data a bit. [2] 43 | 0.0.10.0 07/09/2015 Added date & boolean valdation + filters. [3] 44 | 0.0.9.0 07/09/2015 Added value filtering and path creation. [3.5] 45 | 0.0.7.0 05/09/2015 Working to the point where it can validate and structure the integrationHeader. [2] 46 | 0.0.6.0 05/09/2015 Got basic validation functionality added via the Validator trait. [1.5] 47 | 0.0.5.0 04/09/2015 Ditched multi-class request in favour of a single request builder utility (using the schema). [1.5] 48 | 0.0.5.0 04/09/2015 Started on SOAP connector, but left till we get the auth details for testing. [1.9] 49 | 0.0.3.0 03/09/2015 More work on the structure, started on connectors + switched to atoum and got first test running. [4.0] 50 | 0.0.1.0 01/09/2015 Started stubbing out the basic structure. [1.1] 51 | 0.0.0.0 31/08/2015 Created initial module structure. [1.1] -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turtledesign/royalmail-php", 3 | "description": "PHP module for interfacing with the Royal Mail shipping api : http://www.royalmail.com/corporate/services/shipping-api ", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "shipping", 8 | "postage", 9 | "royal-mail", 10 | "post-office", 11 | "barcodes" 12 | ], 13 | 14 | "autoload": { 15 | "psr-4": { 16 | "RoyalMail\\": "src/" 17 | } 18 | }, 19 | 20 | "authors": [ 21 | { 22 | "name": "Stefan Nesbitt", 23 | "email": "stefan@turtledesign.com" 24 | } 25 | ], 26 | 27 | "require": { 28 | "php": ">=5.4.0", 29 | "symfony/yaml": ">=2.7", 30 | "vlucas/valitron": "~1.2" 31 | }, 32 | 33 | "require-dev": { 34 | "atoum/atoum": "^3.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Data files - country codes and the like. 2 | 3 | When updating these the spot check info in ~/tests/unit/Helper/DataTest.php will need updating to match. -------------------------------------------------------------------------------- /data/bfpo_formats.yml: -------------------------------------------------------------------------------- 1 | "EAA": "SD 0 - 100G £500 COMP" 2 | "EAB": "SD 101 - 500G £500 COMP" 3 | "EAC": "SD 501- 1KG £500 COMP" 4 | "EAD": "SD 1001G - 2KG £500 COMP" 5 | "EBA": "SD 0 - 100G £1000 COMP" 6 | "EBB": "SD 101 - 500G £1000 COMP" 7 | "EBC": "SD 501G- 1KG £1000 COMP" 8 | "EBD": "SD 1001G - 2KG £1000 COMP" 9 | "ECA": "SD 0 - 100G £2500 COMP" 10 | "ECC": "SD 501G- 1KG £2500 COMP" 11 | "ECD": "SD 1001G - 2KG £2500 COMP" 12 | "EVB": "SD 101 - 500G £2500 COMP" 13 | "FAE": "FORCES AEROGRAMMES" 14 | -------------------------------------------------------------------------------- /data/country_codes.yml: -------------------------------------------------------------------------------- 1 | "AC": "ASCENSION" # When updating fix duplicates: CD, CI, FK, GL 2 | "AD": "ANDORRA" 3 | "AE": "UNITED ARAB EMIRATES" 4 | "AF": "AFGHANISTAN" 5 | "AG": "ANTIGUA AND BARBUDA" 6 | "AI": "ANGUILLA" 7 | "AL": "ALBANIA" 8 | "AM": "ARMENIA" 9 | "AN": "NETHERLANDS ANTILLES" 10 | "AO": "ANGOLA" 11 | "AQ": "ANTARTIC" 12 | "AR": "ARGENTINA" 13 | "AS": "AMERICAN SAMOA" 14 | "AT": "AUSTRIA" 15 | "AU": "AUSTRALIA" 16 | "AW": "ARUBA" 17 | "AX": "ALAND ISLAND" 18 | "AZ": "AZERBAIJAN" 19 | "BA": "BOSNIA AND HERZEGOVINA" 20 | "BB": "BARBADOS" 21 | "BD": "BANGLADESH" 22 | "BE": "BELGIUM" 23 | "BF": "BURKINA FASO" 24 | "BG": "BULGARIA" 25 | "BH": "BAHRAIN" 26 | "BI": "BURUNDI" 27 | "BJ": "BENIN" 28 | "BL": "SAINT BARTHELEMY" 29 | "BM": "BERMUDA" 30 | "BN": "BRUNEI DARUSSALAM" 31 | "BO": "BOLIVIA, PLURINATIONAL STATE OF" 32 | "BQ": "BONAIRE" 33 | "BR": "BRAZIL" 34 | "BS": "BAHAMAS" 35 | "BT": "BHUTAN" 36 | "BW": "BOTSWANA" 37 | "BY": "BELARUS" 38 | "BZ": "BELIZE" 39 | "CA": "CANADA" 40 | "CC": "COCOS (KEELING) ISLANDS" 41 | "CD": "CONGO, DEMOCRATIC REPUBLIC OF " 42 | "CF": "CENTRAL AFRICAN REPUBLIC" 43 | "CH": "SWITZERLAND" 44 | "CI": "COTE D'IVOIRE/IVORY COAST" 45 | "CK": "COOK ISLANDS" 46 | "CL": "CHILE" 47 | "CM": "CAMEROON" 48 | "CN": "CHINA, THE PEOPLE'S REPUBLIC OF" 49 | "CO": "COLUMBIA" 50 | "CR": "COSTA RICA" 51 | "CU": "CUBA" 52 | "CV": "CAPE VERDE" 53 | "CW": "CURACAO" 54 | "CX": "CHRISTMAS ISLANDS (INDIAN)" 55 | "CY": "CYPRUS" 56 | "CZ": "CZECH REPUBLIC" 57 | "DE": "GERMANY" 58 | "DJ": "DJIBOUTI" 59 | "DK": "DENMARK" 60 | "DM": "DOMINICA" 61 | "DO": "DOMINICAN REPUBLIC" 62 | "DZ": "ALGERIA" 63 | "EA": "SPANISH NORTH AFRICA" 64 | "EC": "ECUADOR" 65 | "EE": "ESTONIA" 66 | "EG": "EGYPT" 67 | "EH": "WESTERN SAHARA" 68 | "ER": "ERITREA" 69 | "ES": "SPAIN" 70 | "ET": "ETHIOPIA" 71 | "FI": "FINLAND" 72 | "FJ": "FIJI" 73 | "FK": "FALKLAND ISLANDS" 74 | "FM": "MICRONESIA, FEDERATED STATES OF" 75 | "FO": "FAROE ISLANDS" 76 | "FR": "FRANCE" 77 | "GA": "GABON" 78 | "GB": "UNITED KINGDOM" 79 | "GD": "GRENADA" 80 | "GE": "GEORGIA" 81 | "GF": "FRENCH GUIANA" 82 | "GH": "GHANA" 83 | "GI": "GIBRALTAR" 84 | "GL": "GREENLAND" 85 | "GM": "GAMBIA" 86 | "GN": "GUINEA" 87 | "GP": "GUADELOUPE, FRENCH ANTILLES" 88 | "GQ": "EQUATORIAL GUINEA" 89 | "GR": "GREECE" 90 | "GS": "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS" 91 | "GT": "GUATEMALA" 92 | "GU": "GUAM" 93 | "GW": "GUINEA-BISSAU" 94 | "GY": "GUYANA" 95 | "HK": "HONG KONG" 96 | "HN": "HONDURAS" 97 | "HR": "CROATIA" 98 | "HT": "HAITI" 99 | "HU": "HUNGARY" 100 | "IC": "CANARY ISLANDS" 101 | "ID": "INDONESIA" 102 | "IE": "REPUBLIC OF IRELAND" 103 | "IL": "ISRAEL" 104 | "IN": "INDIA" 105 | "IO": "BRITISH INDIAN OCEAN TERRITORY" 106 | "IQ": "IRAQ" 107 | "IR": "IRAN, ISLAMIC REPUBLIC OF" 108 | "IS": "ICELAND" 109 | "IT": "ITALY" 110 | "JM": "JAMAICA" 111 | "JO": "JORDAN" 112 | "JP": "JAPAN" 113 | "KE": "KENYA" 114 | "KG": "KYRGYZSTAN" 115 | "KH": "CAMBODIA" 116 | "KI": "KIRIBATI" 117 | "KM": "COMOROS" 118 | "KN": "SAINT KITTS AND NEVIS" 119 | "KP": "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF" 120 | "KR": "KOREA, REPUBLIC OF" 121 | "KW": "KUWAIT" 122 | "KY": "CAYMAN ISLANDS" 123 | "KZ": "KAZAKHSTAN" 124 | "LA": "LAO PEOPLE'S DEMOCRATIC REPUBLIC" 125 | "LB": "LEBANON" 126 | "LC": "SAINT LUCIA" 127 | "LI": "LIECHTENSTEIN,SWITZERLAND" 128 | "LK": "SRI LANKA" 129 | "LR": "LIBERIA" 130 | "LS": "LESOTHO" 131 | "LT": "LITHUANIA" 132 | "LU": "LUXEMBOURG" 133 | "LV": "LATVIA" 134 | "LY": "LIBYAN ARAB JAMAHIRIYA" 135 | "MA": "MOROCCO" 136 | "MC": "MONACO" 137 | "MD": "MOLDOVA, REPUBLIC OF" 138 | "ME": "MONTENEGRO" 139 | "MF": "SAINT MARTIN (FRENCH PART)" 140 | "MG": "MADAGASCAR" 141 | "MH": "MARSHALL ISLANDS" 142 | "MK": "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF" 143 | "ML": "MALI" 144 | "MM": "BURMA (MYANMAR)" 145 | "MN": "MONGOLIA" 146 | "MO": "MACAO" 147 | "MP": "MARIANA ISLAND, UNITED STATES OF AMERICA" 148 | "MQ": "MARTINIQUE" 149 | "MR": "MAURITANIA" 150 | "MS": "MONTSERRAT" 151 | "MT": "MALTA" 152 | "MU": "MAURITIUS" 153 | "MV": "MALDIVES" 154 | "MW": "MALAWI" 155 | "MX": "MEXICO" 156 | "MY": "MALAYSIA" 157 | "MZ": "MOZAMBIQUE" 158 | "NA": "NAMIBIA" 159 | "NC": "NEW CALEDONIA" 160 | "NE": "NIGER" 161 | "NF": "NORFOLK ISLAND" 162 | "NG": "NIGERIA" 163 | "NI": "NICARAGUA" 164 | "NL": "NETHERLANDS" 165 | "NO": "NORWAY" 166 | "NP": "NEPAL" 167 | "NR": "NAURU" 168 | "NU": "NIUE" 169 | "NZ": "NEW ZEALAND" 170 | "OM": "OMAN" 171 | "PA": "PANAMA" 172 | "PE": "PERU" 173 | "PF": "FRENCH POLYNESIA" 174 | "PG": "PAPUA NEW GUINEA" 175 | "PH": "PHILIPPINES" 176 | "PK": "PAKISTAN" 177 | "PL": "POLAND" 178 | "PN": "PITCAIRN" 179 | "PR": "PUERTO RICO" 180 | "PT": "PORTUGAL" 181 | "PW": "BELAU" 182 | "PY": "PARAGUAY" 183 | "QA": "QATAR" 184 | "RE": "REUNION " 185 | "RO": "ROMANIA" 186 | "RS": "SERBIA" 187 | "RU": "RUSSIAN FEDERATION" 188 | "RW": "RWANDA" 189 | "SA": "SAUDI ARABIA" 190 | "SB": "SOLOMON ISLANDS" 191 | "SC": "SEYCHELLES" 192 | "SD": "SUDAN" 193 | "SE": "SWEDEN" 194 | "SG": "SINGAPORE" 195 | "SH": "ST HELENA" 196 | "SI": "SLOVENIA" 197 | "SJ": "SVALBARD AND JAN MAYEN" 198 | "SK": "SLOVAKIA" 199 | "SL": "SIERRA LEONE" 200 | "SM": "SAN MARINO" 201 | "SN": "SENEGAL" 202 | "SO": "SOMALIA" 203 | "SR": "SURINAME" 204 | "ST": "SAO TOME AND PRINCIPE" 205 | "SV": "EL SALVADOR" 206 | "SY": "SYRIAN ARAB REPUBLIC" 207 | "SZ": "SWAZILAND" 208 | "TA": "TRISTAN DA CUNHA" 209 | "TC": "TURKS AND CAICOS ISLANDS" 210 | "TD": "CHAD" 211 | "TF": "FRENCH SOUTHERN TERRITORIES" 212 | "TG": "TOGO" 213 | "TH": "THAILAND" 214 | "TJ": "TAJIKISTAN" 215 | "TK": "TOKELAU" 216 | "TL": "TIMOR-LESTE" 217 | "TM": "TURKMENISTAN" 218 | "TN": "TUNISIA" 219 | "TO": "TONGA" 220 | "TR": "TURKEY" 221 | "TT": "TRINIDAD AND TOBAGO" 222 | "TV": "TUVALU" 223 | "TW": "TAIWAN" 224 | "TZ": "TANZANIA, UNITED REPUBLIC OF" 225 | "UA": "UKRAINE" 226 | "UG": "UGANDA" 227 | "UM": "UNITED STATES OF AMERICA MINOR OUTLYING ISLANDS" 228 | "US": "UNITED STATES OF AMERICA" 229 | "UY": "URUGUAY" 230 | "UZ": "UZBEKISTAN" 231 | "VA": "HOLY SEE (VATICAN CITY STATE)" 232 | "VC": "SAINT VINCENT AND THE GRENADINES" 233 | "VE": "VENEZUELA, BOLIVARIAN REPUBLIC OF" 234 | "VG": "VIRGIN ISLANDS, BRITISH" 235 | "VI": "VIRGIN ISLANDS, U.S." 236 | "VN": "VIET NAM" 237 | "VU": "VANUATU" 238 | "WF": "WALLIS AND FUTUNA" 239 | "WS": "SAMOA" 240 | "XA": "BRITISH ANTARTIC TERRITORY" 241 | "XB": "FRENCH SOUTH ANTARTIC TERRITOR" 242 | "XC": "NEW ZEALAND ANTARCTIC TERRITORY" 243 | "XD": "NORWEGIAN ANTARTIC TERRITORY" 244 | "XE": "LINE ISLAND, USA OF AMERICA" 245 | "XF": "GUANTANAMO BAY, UNITED STATES OF AMERICA" 246 | "XG": "HAWAII, UNITED STATES OF AMERICA" 247 | "XH": "CHRISTMAS ISLANDS (PACIFIC), KIRIBATI" 248 | "XI": "FANNING ISLAND, KIRIBATI" 249 | "XJ": "PHOENIX ISLAND, KIRIBATI" 250 | "XK": "WASHINGTON ISLAND, KIRIBATI" 251 | "XL": "ST EUSTATIAS" 252 | "XM": "KEELING ISLAND, AUSTRALIA" 253 | "XN": "NORTHERN MARIANA ISLANDS" 254 | "XO": "NEW HEBRIDES, VANUATU" 255 | "XZ": "KOSOVO" 256 | "YE": "YEMEN" 257 | "YT": "MAYOTTE" 258 | "ZA": "SOUTH AFRICA" 259 | "ZM": "ZAMBIA" 260 | "ZW": "ZIMBABWE" 261 | -------------------------------------------------------------------------------- /data/international_documents.yml: -------------------------------------------------------------------------------- 1 | CN22: "Customs Declaration CN22 - value up to £270" 2 | CN23: "Customs Declaration CN23 - value over £270" 3 | CI: "Commercial Invoice" -------------------------------------------------------------------------------- /data/label_formats.yml: -------------------------------------------------------------------------------- 1 | PDF: Standard Base64 Encoded PDF Label. 2 | PNG: Base64 Encoded PNG images of the 2D Data Matric and 1D Linear Barcode. 3 | DS: Data stream. 4 | DSPDF: Both the data stream and the Base64 Encoded PDF Label. 5 | DSPNG: Both the data stream and the Base64 Encoded PNG images of the 2D Data Matric and 1D Linear Barcode. -------------------------------------------------------------------------------- /data/service_enhancements.yml: -------------------------------------------------------------------------------- 1 | "1": "Consequential Loss £1000 - Consequential Loss Insurance" 2 | "2": "Consequential Loss £2500 - Consequential Loss Insurance" 3 | "3": "Consequential Loss £5000 - Consequential Loss Insurance" 4 | "4": "Consequential Loss £7500 - Consequential Loss Insurance" 5 | "5": "Consequential Loss £10000 - Consequential Loss Insurance" 6 | "6": "Recorded Signed For Mail" 7 | "11": "Consequential Loss £750 - Consequential Loss Insurance" 8 | "12": "Tracked Signature - Tracked Delivery Option" 9 | "13": "SMS Notification" 10 | "14": "E-Mail Notification" 11 | "15": "Safeplace - Tracked Delivery Option" 12 | "16": "SMS & E-Mail Notification" 13 | "22": "Local Collect" 14 | "24": "Saturday Guaranteed" 15 | -------------------------------------------------------------------------------- /data/service_formats.yml: -------------------------------------------------------------------------------- 1 | "F": "Inland Large Letter" 2 | "L": "Inland Letter" 3 | "N": "Inland format Not Applicable" 4 | "P": "Inland Parcel" 5 | "E.int": "International Parcel" # CAVEAT: The keys for international formats need suffixing to avoid duplicates. 6 | "G.int": "International Large Letter" 7 | "N.int": "International Format Not Applicable" 8 | "P.int": "International Letter" 9 | -------------------------------------------------------------------------------- /data/service_offerings.yml: -------------------------------------------------------------------------------- 1 | "STL": "1ST AND 2ND CLASS ACCOUNT MAIL" # Duplicated in original data. 2 | "TPL": "ROYAL MAIL TRACKED 48 (HV)" 3 | "TPN": "ROYAL MAIL TRACKED 24" 4 | "TPS": "ROYAL MAIL TRACKED 48" 5 | "TRM": "ROYAL MAIL TRACKED 24 (HV)" 6 | "TRN": "ROYAL MAIL TRACKED 24 (LBT)" 7 | "TRS": "ROYAL MAIL TRACKED 48 (LBT)" 8 | "TSN": "ROYAL MAIL TRACKED RETURNS 24" 9 | "TSS": "ROYAL MAIL TRACKED RETURNS 48" 10 | "WE1": "INTL BUS PARCELS ZERO SORT PRIORITY" 11 | "WE3": "INTL BUS PARCELS ZERO SORT ECONOMY" 12 | "WG1": "INTL BUS MAIL LRG LTR ZERO SRT PRIORITY" 13 | "WG3": "INTL BUS MAIL LRG LTR ZERO SORT ECONOMY" 14 | "WG4": "INTL BUS MAIL LRG LTR ZERO SRT PRI MCH" 15 | "WG6": "INTL BUS MAIL L LTR ZERO SRT ECONOMY MCH" 16 | "WW1": "INTL BUS MAIL MIXED ZERO SORT PRIORITY" 17 | "WW3": "INTL BUS MAIL MIXED ZERO SORT ECONOMY" 18 | "WW4": "INTL BUS MAIL MIXED ZERO SORT PRI MCH" 19 | "WW6": "INTL BUS MAIL MIXD ZERO SRT ECONOMY MCH" 20 | "ZC1": "INTL BUS MAIL MIXED ZERO SORT PREMIUM" 21 | -------------------------------------------------------------------------------- /data/service_types.yml: -------------------------------------------------------------------------------- 1 | "1": "Royal Mail 24 / 1st Class" 2 | "2": "Royal Mail 48 / 2nd Class" 3 | "D": "Special Delivery Guaranteed" 4 | "H": "HM Forces (BFPO)" 5 | "I": "International" 6 | "R": "Tracked Returns" 7 | "T": "Royal Mail Tracked" 8 | -------------------------------------------------------------------------------- /examples/BasicUsageTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($interface instanceof RoyalMail\RoyalMail); 19 | 20 | $dev_helper = $interface->getDevelopmentHelper(); 21 | 22 | $this->assertTrue($dev_helper instanceof RoyalMail\Helper\Development); 23 | 24 | $req_params = $dev_helper->getSampleRequest('cancelShipment'); 25 | 26 | $this->assertTrue(is_array($req_params) && count($req_params) > 0); 27 | 28 | $response = $interface->cancelShipment($req_params); 29 | // $interface->processAction('cancelShipment', $req_params); // is an equivalent alternative syntax to call an API action. 30 | 31 | $this->assertTrue($response instanceof RoyalMail\Response\Interpreter); 32 | 33 | $this->assertTrue($response->succeeded()); 34 | 35 | $this->assertEquals('RQ221150275GB', $response['cancelled_shipments'][0]); 36 | } 37 | } -------------------------------------------------------------------------------- /reference/CDM/CommonIntegrationSchemaV1_11.xsd: -------------------------------------------------------------------------------- 1 | 2 | 41 | 42 | 43 | 44 | 45 | 46 | For a request, this is the ID of the Service Requester calling the service, unique when combined with the TransactionId. For the response, this is the ID of the Service Provider, unique when combined with the TransactionId. 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Id preserved throughout the lifespan of the transaction, unique when combined with the applicationId and intermediaryId (if applicable). 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | User ID associated with the end user of the internal or external organisation 71 | 72 | 73 | 74 | 75 | The ID of the Service Requester system calling the service, unique when combined with the transactionId. 76 | 77 | 78 | 79 | 80 | Id associated with the Intermediary application (if applicable). 81 | 82 | 83 | 84 | 85 | Unique session id sent by the Service Requestor (for the request) or returned by the Service Provider (for the response) 86 | 87 | 88 | 89 | 90 | 91 | 92 | Common integration header definition 93 | 94 | 95 | 96 | 97 | Date timestamp associated with when the data was generated, sent or received 98 | 99 | 100 | 101 | 102 | Version of the service 103 | 104 | 105 | 106 | 107 | Identification details associated with the Service Requestor or Service Provider 108 | 109 | 110 | 111 | 112 | Flag which can be used to test that the exposed service is available. It is not expected that this flag will be used under normal BAU service operation. 113 | 114 | 115 | 116 | 117 | Flag which can be used to allow any messages to be dynamically debugged. It is not expected that this flag will be used under normal BAU service operation. 118 | 119 | 120 | 121 | 122 | Flag which can be used to track the performance of the service across RMG systems (entry time, exit time, latency etc). 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Code associated with the error condition in the form ENNNN 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Code associated with the warning condition in the form WNNNN 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Code associated with the error condition 149 | 150 | 151 | 152 | 153 | Description of the error condition 154 | 155 | 156 | 157 | 158 | Cause of the business error (if known) 159 | 160 | 161 | 162 | 163 | Description of the resolution and action required to correct the error 164 | 165 | 166 | 167 | 168 | Context of the business error, e.g. client or server 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | Code associated with the warning 183 | 184 | 185 | 186 | 187 | Description of the warning 188 | 189 | 190 | 191 | 192 | Cause of the warning (if known) 193 | 194 | 195 | 196 | 197 | Description of the resolution and action required to correct the warning 198 | 199 | 200 | 201 | 202 | Context of the warning, e.g. client or server 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | Common integration footer definition 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /reference/CDM/DatatypesV2_3.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Used in the digital representation of 2D Barcodes 6 | 7 | 8 | 9 | 10 | 11 | Boolean, or boolean logic, is a subset of algebra used for creating true/false statements. Boolean expressions use the operators AND, OR, XOR, and NOT to compare values and return a true or false result. Boolean values can also be referred to as 'flags'. 12 | 13 | 14 | 15 | 16 | 17 | A number indicating a whole number amount as in a count of items. 18 | 19 | 20 | 21 | 22 | 23 | Used in the digital representation of documents 24 | 25 | 26 | 27 | 28 | 29 | A comment is a sub-type of string which can be used as a note intended as an explanation, illustration, instruction, capture of an interaction or an annotation. This is free format text which can be input by the end user. 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | A date represents a period in time which could be a particular month, day, and year at which some event happened or will happen. 38 | 39 | 40 | 41 | 42 | 43 | dateTime represents a period in time which could be a particular month, day, and year at which some event happened or will happen but also includes a breakdown of time using hours, minutes, seconds and milliseconds. A dateTime stamp is a common use for this data type so that a precise record of an event can be captured. 44 | 45 | 46 | 47 | 48 | 49 | A decimal data type represents a real number with every decimal place indicating a multiple of a negative power of 10. It represents non-repeating decimal fractions like 0.3 and -1.17 without rounding, and gives the ability to do arithmetic on them. 50 | 51 | 52 | 53 | 54 | 55 | A description is a sub-type of string and is statement in words that gives an account or descriptive representation of a thing. Descriptions are only editable via an admin function and are for display and information purposes only for end users. 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | A float describes a data type for representing an approximation to real numbers in a way that can support a wide range of values. The numbers are, in general, represented approximately to a fixed number of significant digits (the mantissa) and scaled using an exponent. The base for the scaling is normally 2, 10 or 16. 64 | For example, the numbers 5.5, 0.001, and -2,345.6789 are floating point numbers. 65 | 66 | 67 | 68 | 69 | 70 | A decimal data type representing Latitude and Longitude data. The format of decimal 10,7 was chosen as a generic representation which facilities conversion between geospatial systems 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | An identifier is a sub-type of string which serves as a means of identification for a particular thing; An identifier can be associated with a name. 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | An integer is a whole number (not a fraction) that can be positive, negative, or zero. Unlike floats, integers cannot have decimal places. 88 | 89 | 90 | 91 | 92 | 93 | A long description is a sub-type of string and is a longer version of a description (see definition of description) and is used to expand a description. Descriptions are only editable via an admin function and are for display and information purposes only for end users. 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | A long name is a sub-type of string and is word or a combination of words by which an organization, place, or thing, a body or class, or any object of thought is designated, called, or known. 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | A name is a sub-type of string and is word or a combination of words by which a person, place, or thing, a body or class, or any object of thought is designated, called, or known. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | A number indicating a position as in a sequence number. 118 | 119 | 120 | 121 | 122 | 123 | A short description is a sub-type of string and is a shorter version of a description (see definition of description) and is used to summarise a longer description. Descriptions are only editable via an admin function and are for display and information purposes only for end users 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | A name is a sub-type of string and is word or a combination of words by which a person, place, or thing, a body or class, or any object of thought is designated, called, or known. short name is intended for use with Reference data items 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Simple set of alphanumeric characters. 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | time is a unit of measure to record hours, minutes, seconds and milliseconds. 148 | 149 | 150 | 151 | 152 | 153 | Used in the digital representation of 2D Barcodes 154 | ### DO NOT USE - superceded by twoDBarcode ### 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /reference/CDM/RMGSOAPExtensionsV1_2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 22 | Element used to hold SOAP fault details 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Used to hold the RMG defined data elements associated with the SOAP fault 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /reference/README.md: -------------------------------------------------------------------------------- 1 | # API Docs 2 | 3 | > Contains reference copies of the API docs that the version/branch was built on. 4 | 5 | Latest versions are on the [Royal Mail Shipping API Service Page](http://www.royalmail.com/corporate/services/shipping-api) -------------------------------------------------------------------------------- /reference/RMG_Shipping_API_(SOAP)_Technical_User_Guide_v2_4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/RMG_Shipping_API_(SOAP)_Technical_User_Guide_v2_4.pdf -------------------------------------------------------------------------------- /reference/SAPI-Reference-Data-v2-2_01-June-2015.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/SAPI-Reference-Data-v2-2_01-June-2015.xlsx -------------------------------------------------------------------------------- /reference/oasis-200401-wss-wssecurity-secext-1.0.xsd: -------------------------------------------------------------------------------- 1 |  2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | This type represents an element with arbitrary attributes. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | This type is used for password elements per Section 4.1. 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | This type is used for elements containing stringified binary data. 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | This type represents a username token per Section 4.1 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | A security token that is encoded in binary 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | A security token key identifier 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Typedef to allow a list of usages (as URIs). 79 | 80 | 81 | 82 | 83 | 84 | This global attribute is used to indicate the usage of a referenced or indicated token within the containing context 85 | 86 | 87 | 88 | 89 | This type represents a reference to an external security token. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | This type represents a reference to an embedded security token. 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | This type is used reference a security token. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | This complexType defines header block to use for security-relevant data directed at a specific SOAP actor. 119 | 120 | 121 | 122 | 123 | The use of "any" is to allow extensibility and different forms of security data. 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | This complexType defines a container for elements to be specified from any namespace as properties/parameters of a DSIG transformation. 132 | 133 | 134 | 135 | 136 | The use of "any" is to allow extensibility from any namespace. 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | This element defines the wsse:UsernameToken element per Section 4.1. 145 | 146 | 147 | 148 | 149 | This element defines the wsse:BinarySecurityToken element per Section 4.2. 150 | 151 | 152 | 153 | 154 | This element defines a security token reference 155 | 156 | 157 | 158 | 159 | This element defines a security token embedded reference 160 | 161 | 162 | 163 | 164 | This element defines a key identifier reference 165 | 166 | 167 | 168 | 169 | This element defines the wsse:SecurityTokenReference per Section 4.3. 170 | 171 | 172 | 173 | 174 | This element defines the wsse:Security SOAP header element per Section 4. 175 | 176 | 177 | 178 | 179 | This element contains properties for transformations from any namespace, including DSIG. 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /reference/oasis-200401-wss-wssecurity-utility-1.0.xsd: -------------------------------------------------------------------------------- 1 |  2 | 10 | 16 | 17 | 18 | 19 | 20 | This type defines the fault code value for Timestamp message expiration. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | This global attribute supports annotating arbitrary elements with an ID. 32 | 33 | 34 | 35 | 36 | 37 | 38 | Convenience attribute group used to simplify this schema. 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | This type is for elements whose [children] is a psuedo-dateTime and can have arbitrary attributes. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | This type is for elements whose [children] is an anyURI and can have arbitrary attributes. 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | This complex type ties together the timestamp related elements into a composite type. 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | This element allows Timestamps to be applied anywhere element wildcards are present, 89 | including as a SOAP header. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | This element allows an expiration time to be applied anywhere element wildcards are present. 98 | 99 | 100 | 101 | 102 | 103 | 104 | This element allows a creation time to be applied anywhere element wildcards are present. 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /reference/responses/cancelShipmentErrorAndWarningResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-02-09T09:35:28 6 | 2 7 | 8 | 111111113 9 | 420642961 10 | 11 | 12 | 13 | 14 | 15 | 16 | Cancelled 17 | 18 | 19 | 2015-02-09T10:35:28.000+02:00 20 | 21 | 22 | RQ221150275GB 23 | 24 | 25 | 26 | 27 | 28 | E1084 29 | shipmentType is a required field 30 | 31 | 32 | 33 | 34 | W1084 35 | shipmentType would be nice to have... 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /reference/responses/cancelShipmentMultipleErrorResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-02-09T09:35:28 6 | 2 7 | 8 | 111111113 9 | 420642961 10 | 11 | 12 | 13 | 14 | 15 | 16 | Cancelled 17 | 18 | 19 | 2015-02-09T10:35:28.000+02:00 20 | 21 | 22 | RQ221150275GB 23 | 24 | 25 | 26 | 27 | 28 | E1084 29 | shipmentType is a required field 30 | 31 | 32 | E1085 33 | another error 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /reference/responses/cancelShipmentMultipleWarningResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-02-09T09:35:28 6 | 2 7 | 8 | 111111113 9 | 420642961 10 | 11 | 12 | 13 | 14 | 15 | 16 | Cancelled 17 | 18 | 19 | 2015-02-09T10:35:28.000+02:00 20 | 21 | 22 | RQ221150275GB 23 | 24 | 25 | 26 | 27 | 28 | W1084 29 | shipmentType would be nice to have... 30 | 31 | 32 | W1085 33 | another warning 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /reference/responses/cancelShipmentResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/cancelShipmentResponse.xml -------------------------------------------------------------------------------- /reference/responses/cancelShipmentSingleErrorResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-02-09T09:35:28 6 | 2 7 | 8 | 111111113 9 | 420642961 10 | 11 | 12 | 13 | 14 | 15 | 16 | Cancelled 17 | 18 | 19 | 2015-02-09T10:35:28.000+02:00 20 | 21 | 22 | RQ221150275GB 23 | 24 | 25 | 26 | 27 | 28 | E1084 29 | shipmentType is a required field 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /reference/responses/cancelShipmentSingleWarningResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-02-09T09:35:28 6 | 2 7 | 8 | 111111113 9 | 420642961 10 | 11 | 12 | 13 | 14 | 15 | 16 | Cancelled 17 | 18 | 19 | 2015-02-09T10:35:28.000+02:00 20 | 21 | 22 | RQ221150275GB 23 | 24 | 25 | 26 | 27 | 28 | W1084 29 | shipmentType would be nice to have... 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /reference/responses/createManifestResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/createManifestResponse.xml -------------------------------------------------------------------------------- /reference/responses/createShipmentInternationalResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/createShipmentInternationalResponse.xml -------------------------------------------------------------------------------- /reference/responses/createShipmentResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/createShipmentResponse.xml -------------------------------------------------------------------------------- /reference/responses/printDocumentResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/printDocumentResponse.xml -------------------------------------------------------------------------------- /reference/responses/printLabelResponse(International - Datastream with Images).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/printLabelResponse(International - Datastream with Images).xml -------------------------------------------------------------------------------- /reference/responses/printLabelResponse(International with Localised Address).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/printLabelResponse(International with Localised Address).xml -------------------------------------------------------------------------------- /reference/responses/printLabelResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/printLabelResponse.xml -------------------------------------------------------------------------------- /reference/responses/printManifestResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/printManifestResponse.xml -------------------------------------------------------------------------------- /reference/responses/request1DRangesResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/request1DRangesResponse.xml -------------------------------------------------------------------------------- /reference/responses/request2DItemIDRangeResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/request2DItemIDRangeResponse.xml -------------------------------------------------------------------------------- /reference/responses/updateShipmentResponse.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/reference/responses/updateShipmentResponse.xml -------------------------------------------------------------------------------- /reference/xml.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | See http://www.w3.org/XML/1998/namespace.html and 8 | http://www.w3.org/TR/REC-xml for information about this namespace. 9 | 10 | This schema document describes the XML namespace, in a form 11 | suitable for import by other schema documents. 12 | 13 | Note that local names in this namespace are intended to be defined 14 | only by the World Wide Web Consortium or its subgroups. The 15 | following names are currently defined in this namespace and should 16 | not be used with conflicting semantics by any Working Group, 17 | specification, or document instance: 18 | 19 | base (as an attribute name): denotes an attribute whose value 20 | provides a URI to be used as the base for interpreting any 21 | relative URIs in the scope of the element on which it 22 | appears; its value is inherited. This name is reserved 23 | by virtue of its definition in the XML Base specification. 24 | 25 | lang (as an attribute name): denotes an attribute whose value 26 | is a language code for the natural language of the content of 27 | any element; its value is inherited. This name is reserved 28 | by virtue of its definition in the XML specification. 29 | 30 | space (as an attribute name): denotes an attribute whose 31 | value is a keyword indicating what whitespace processing 32 | discipline is intended for the content of the element; its 33 | value is inherited. This name is reserved by virtue of its 34 | definition in the XML specification. 35 | 36 | Father (in any context at all): denotes Jon Bosak, the chair of 37 | the original XML Working Group. This name is reserved by 38 | the following decision of the W3C XML Plenary and 39 | XML Coordination groups: 40 | 41 | In appreciation for his vision, leadership and dedication 42 | the W3C XML Plenary on this 10th day of February, 2000 43 | reserves for Jon Bosak in perpetuity the XML name 44 | xml:Father 45 | 46 | 47 | 48 | 49 | This schema defines attributes and an attribute group 50 | suitable for use by 51 | schemas wishing to allow xml:base, xml:lang or xml:space attributes 52 | on elements they define. 53 | 54 | To enable this, such a schema must import this schema 55 | for the XML namespace, e.g. as follows: 56 | <schema . . .> 57 | . . . 58 | <import namespace="http://www.w3.org/XML/1998/namespace" 59 | schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> 60 | 61 | Subsequently, qualified reference to any of the attributes 62 | or the group defined below will have the desired effect, e.g. 63 | 64 | <type . . .> 65 | . . . 66 | <attributeGroup ref="xml:specialAttrs"/> 67 | 68 | will define a type which will schema-validate an instance 69 | element with any of those attributes 70 | 71 | 72 | 73 | In keeping with the XML Schema WG's standard versioning 74 | policy, this schema document will persist at 75 | http://www.w3.org/2001/03/xml.xsd. 76 | At the date of issue it can also be found at 77 | http://www.w3.org/2001/xml.xsd. 78 | The schema document at that URI may however change in the future, 79 | in order to remain compatible with the latest version of XML Schema 80 | itself. In other words, if the XML Schema namespace changes, the version 81 | of this document at 82 | http://www.w3.org/2001/xml.xsd will change 83 | accordingly; the version at 84 | http://www.w3.org/2001/03/xml.xsd will not change. 85 | 86 | 87 | 88 | 89 | 90 | In due course, we should install the relevant ISO 2- and 3-letter 91 | codes as the enumerated possible values . . . 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | See http://www.w3.org/TR/xmlbase/ for 107 | information about this attribute. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /reference/xmldsig-core-schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./vendor/bin/atoum -bf tests/Bootstrap.php -d tests/unit -------------------------------------------------------------------------------- /src/Connector/MockSoapClient.php: -------------------------------------------------------------------------------- 1 | config['static_responses'] . '/' . $action . $this->postfix); 17 | } 18 | 19 | 20 | function setPostfix($postfix) { 21 | $this->postfix = $postfix; 22 | 23 | return $this; 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /src/Connector/TDSoapClient.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | 17 | parent::__construct($wsdl, array_merge(['trace' => 1], @$config['soap_client_options'] ?: [])); 18 | 19 | if (isset($config['username'])) $this->addWSSecurityHeader($config); 20 | } 21 | 22 | 23 | /** 24 | * Add the WSSE security header { https://msdn.microsoft.com/en-gb/library/ms977327.aspx } 25 | * 26 | * c.f. http://php.net/manual/en/soapclient.soapclient.php#114976 and others in thread. 27 | */ 28 | function addWSSecurityHeader($config) { 29 | $config = array_merge(['timezone' => 'BST'], $config); 30 | 31 | $nonce = $this->getNonce(); 32 | $ts = $this->getTimestamp($config['timezone']); 33 | 34 | $auth = new \stdClass(); 35 | $auth->Username = new \SoapVar($config['username'], XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); 36 | $auth->Password = new \SoapVar($this->getPasswordDigest($nonce, $ts, $config['password']), XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); 37 | $auth->Nonce = new \SoapVar(base64_encode($nonce), XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); 38 | $auth->Created = new \SoapVar($ts, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wsu_ns); 39 | 40 | $username_token = new \stdClass(); 41 | $username_token->UsernameToken = new \SoapVar(@$auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns); 42 | 43 | $security_sv = new \SoapVar( 44 | new \SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns), 45 | SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns 46 | ); 47 | 48 | $this->__setSoapHeaders([new \SoapHeader($this->wss_ns, 'Security', $security_sv, TRUE)]); 49 | } 50 | 51 | 52 | function getNonce() { return mt_rand(); } 53 | 54 | 55 | function getTimestamp($tz) { return (new \DateTime('now', new \DateTimeZone($tz)))->format('Y-m-d\TH:i:s.000\Z'); } 56 | 57 | 58 | function getPasswordDigest($nonce, $ts, $pass) { 59 | $nonce_date_pwd = pack("A*",$nonce) . pack("A*",$ts) . pack("H*", sha1($pass)); 60 | return base64_encode(pack('H*', sha1($nonce_date_pwd))); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Connector/baseConnector.php: -------------------------------------------------------------------------------- 1 | configure(array_merge($this->adaptor_defaults, $config)); 13 | } 14 | 15 | 16 | function request($request_type, $params = [], $config = []) { 17 | return $this->doRequest($request_type, $params, $config); 18 | } 19 | 20 | 21 | /** 22 | * Return the actual request as sent to the API. 23 | * 24 | * @return string 25 | */ 26 | abstract function getAPIFormattedRequest(); 27 | 28 | 29 | abstract protected function doRequest($request_type, $params = [], $config = []); 30 | 31 | 32 | function configure($config) { 33 | $this->config = $config; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Connector/soapConnector.php: -------------------------------------------------------------------------------- 1 | '\RoyalMail\Connector\TDSoapClient', 14 | ], 15 | 16 | $request_input = []; 17 | 18 | 19 | function __construct($config = []) { 20 | parent::__construct($config); 21 | 22 | if (isset($config['endpoint'])) $this->setEndpoint($config['endpoint']); 23 | 24 | if (isset($config['soap_client'])) $this->loadSoapClient($this->config); 25 | } 26 | 27 | 28 | /** 29 | * Send off the request to the Royal Mail API 30 | * 31 | * @see baseConnector::doRequest() 32 | * 33 | * @return \RoyalMail\Response\baseResponse Response class for the request sent. 34 | */ 35 | function doRequest($action, $params = [], $config = []) { 36 | $this->request_input = ['action' => $action, 'parameters' => $params]; 37 | 38 | return $this->getSoapClient($config)->__soapCall($action, [$params]); 39 | } 40 | 41 | 42 | function getDebugInfo() { 43 | return [ 44 | 'config' => $this->config, 45 | 'request_input' => $this->request_input, 46 | 'sent_request' => $this->getAPIFormattedRequest(), 47 | 'sent_headers' => $this->getSoapClient()->__getLastRequestHeaders(), 48 | 'response' => $this->getSoapClient()->__getLastResponse(), 49 | 'response_headers' => $this->getSoapClient()->__getLastResponseHeaders(), 50 | ]; 51 | } 52 | 53 | 54 | function getAPIFormattedRequest() { 55 | return $this->getSoapClient()->__getLastRequest(); 56 | } 57 | 58 | 59 | function getSoapClient($config = NULL) { 60 | if (empty($this->soap_client)) $this->loadSoapClient(empty($config) ? $this->config : array_merge($this->config, $config)); 61 | 62 | return $this->soap_client; 63 | } 64 | 65 | 66 | function loadSoapClient($config) { 67 | $config = array_merge(['endpoint' => $this->getEndpoint()], $config); 68 | 69 | $this->setSoapClient(new $config['soap_client']($config['endpoint'], $config)); 70 | 71 | return $this; 72 | } 73 | 74 | 75 | function setSoapClient($client) { 76 | $this->soap_client = $client; 77 | 78 | return $this; 79 | } 80 | 81 | 82 | function setEndpoint($endpoint) { 83 | $this->endpoint = $endpoint; 84 | 85 | return $this; 86 | } 87 | 88 | 89 | function getEndpoint() { 90 | return $this->endpoint; 91 | } 92 | 93 | 94 | 95 | /** 96 | * Make sure the response from the RM API seems kosher 97 | * 98 | * @throws \RoyalMail\Exception\ResponseException 99 | */ 100 | protected function verifyResponse($response) { 101 | // Verify WS security values. 102 | 103 | return $response; 104 | } 105 | } -------------------------------------------------------------------------------- /src/Exception/RequestException.php: -------------------------------------------------------------------------------- 1 | getMessage()); 11 | 12 | $this->debug_info = $debug; 13 | } 14 | 15 | 16 | function getConnectionDebugInfo() { 17 | return $this->debug_info; 18 | } 19 | 20 | 21 | function getSoapFault() { 22 | return $this->get('exception'); 23 | } 24 | 25 | 26 | function get($key) { 27 | return (isset($this->debug_info[$key])) ? $this->debug_info[$key] : NULL; 28 | } 29 | 30 | 31 | function __call($method, $args) { 32 | $key = preg_replace_callback('/[A-Z]/', function ($m) { return '_' . strtolower($m[0]); }, lcfirst(preg_replace('/^get/', '', $method))); 33 | 34 | if (isset($this->debug_info[$key])) return $this->get($key); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Exception/RoyalMailException.php: -------------------------------------------------------------------------------- 1 | error_list = $errors; 12 | 13 | $this->message = "Errors found with the following request keys: " . implode(', ', $this->error_list); 14 | 15 | return $this; 16 | } 17 | 18 | 19 | function getErrors() { return $this->error_list; } 20 | 21 | 22 | function toJSON() { 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /src/Exception/StructureSkipFieldException.php: -------------------------------------------------------------------------------- 1 | [ 19 | '_default' => ['filter' => 'Default', 'value_key' => 'value'], 20 | '_trim' => ['filter' => 'Trim', 'value_key' => 'apply'], 21 | ], 22 | 23 | 'post' => [ 24 | '_skip_blank' => ['filter' => 'SkipBlank', 'value_key' => 'apply'], 25 | ], 26 | ]; 27 | 28 | 29 | static function filter($val, $schema, $type = 'pre', $helper = NULL) { 30 | 31 | foreach (self::parseFilters($schema, $type) as $filter => $settings) { 32 | 33 | if ($settings === FALSE || @$settings['apply'] === FALSE) continue; // allow turn-off overrides. 34 | 35 | $val = call_user_func_array('self::do' . $filter, [$val, $settings, $helper]); 36 | } 37 | 38 | return $val; 39 | } 40 | 41 | 42 | 43 | static function parseFilters($schema, $type) { 44 | $filters = isset($schema['_' . $type . '_filter']) ? $schema['_' . $type . '_filter'] : []; 45 | 46 | if (! is_array($filters)) $filters = [$filters => []]; // Allow adding single filters by name only. 47 | 48 | return self::parseShortcodes($filters, $schema, $type); 49 | } 50 | 51 | 52 | static function parseShortcodes($filters, $schema, $type) { 53 | foreach (self::$short_codes[$type] as $code => $apply) { 54 | if (isset($schema[$code])) { 55 | $filters = array_merge([ # Shortcodes are merged in, so explicitly specified filters will take priority. 56 | $apply['filter'] => is_array($schema[$code]) 57 | ? $schema[$code] 58 | : [$apply['value_key'] => $schema[$code]] 59 | ], $filters); 60 | } 61 | } 62 | 63 | return $filters; 64 | } 65 | 66 | 67 | static function doTrim($val, $settings, $helper = NULL) { return trim($val); } 68 | 69 | 70 | 71 | static function doDefault($val, $settings, $helper = NULL) { 72 | return empty($val) ? $settings['value'] : $val; // FIXME: replaces all falsy values, better to make that configurable 73 | } 74 | 75 | 76 | static function doBool($val, $settings, $helper = NULL) { 77 | if (is_string($val) && preg_match('/^(No|N|0|FALSE)$/i', $val)) return FALSE; 78 | 79 | return (bool) $val; 80 | } 81 | 82 | 83 | static function doInt($val, $settings, $helper = NULL) { return (int) $val; } 84 | 85 | 86 | static function doGenerateDate($val, $settings, $helper = NULL) { 87 | if (is_string($settings)) $settings = ['format' => $settings]; 88 | 89 | if (! empty($settings['empty_only']) && ! empty($val)) return $val; 90 | 91 | return date_create()->format(@$settings['format'] ?: 'Y-m-d\TH:i:s'); 92 | } 93 | 94 | 95 | static function doFormatDate($val, $settings, $helper = NULL) { 96 | if (is_string($settings)) $settings = ['format' => $settings]; 97 | 98 | return ($val instanceof DateTime) ? $val->format($settings['format']) : $val; 99 | } 100 | 101 | 102 | static function doObjectifyDate($val, $settings, $helper) { // DISCLAIMER: Method name is not a lifestyle recomendation. 103 | if (! empty($helper['params']['text_only'])) return $val; 104 | 105 | try { 106 | return (isset($settings['from_format'])) ? date_create_from_format($val) : date_create($val); 107 | 108 | } catch (Exception $e) { return $val; } // Not a valid date, pass through as this isn't a validator. 109 | } 110 | 111 | 112 | static function doIsEqual($val, $settings, $helper) { 113 | return $val === $settings; 114 | } 115 | 116 | 117 | static function doIsOver($val, $settings, $helper) { 118 | return (is_numeric($val) && $val > $settings); 119 | } 120 | 121 | 122 | static function doTruncate($val, $settings, $helper = NULL) { 123 | if (is_scalar($settings)) $settings = ['length' => $settings]; 124 | 125 | return substr($val, 0, $settings['length']); 126 | } 127 | 128 | 129 | static function doRound($val, $settings, $helper = NULL) { 130 | if (! empty($settings['dp'])) return (float) round($val, $settings['dp']); 131 | 132 | return (int) round($val); 133 | } 134 | 135 | 136 | static function doSkipBlank($val, $settings = NULL, $helper = NULL) { 137 | if (! self::hasValue(['val' => $val], 'val')) throw new SkipException('Skipping blank field'); 138 | 139 | return $val; 140 | } 141 | 142 | 143 | static function doCleanGBPhone($val, $settings, $helper = NULL) { 144 | if (! empty($settings['stripCountryCode'])) { 145 | $val = preg_replace('/^\s*(\+|00)\d\d\s*/', '0', $val); 146 | $val = preg_replace('/^0?\s*\(0\)\s*/', '', $val); 147 | $val = preg_replace('/^0+/', '0', $val); 148 | } 149 | 150 | if (! empty($settings['stripBlanks'])) $val = preg_replace('/\s+/', '', $val); 151 | 152 | return $val; 153 | } 154 | 155 | 156 | static function doFormatGBPostcode($val, $settings = NULL, $helper = NULL) { 157 | if ((! isset($settings['check_country'])) || self::checkPath($settings['check_country'], ['in' => ['GB']], $helper)) { 158 | $val = strtoupper($val); 159 | 160 | if (strpos($val, ' ') != (strlen($val) - 2)) { 161 | $val = str_replace(' ', '', $val); 162 | $val = preg_replace('/(.+)(.{3})$/', '$1 $2', $val); 163 | } 164 | } 165 | 166 | return $val; 167 | } 168 | 169 | 170 | /** 171 | * Skip this field if another field is empty - works on input array so far, can add output test later. 172 | */ 173 | static function doSkipThisIfThatEmpty($val, $settings, $helper = NULL) { 174 | if (is_string($settings)) $settings = ['that' => $settings]; 175 | 176 | if (! self::checkPath($settings['that'], [], $helper)) throw new SkipException('Skipping as ' . $settings['that'] . ' is blank'); 177 | 178 | return $val; 179 | } 180 | 181 | } -------------------------------------------------------------------------------- /src/Helper/Data.php: -------------------------------------------------------------------------------- 1 | ['Delivery' => 'Delivery', 'Return' => 'Return'], 16 | ], $init)); 17 | } 18 | 19 | 20 | function offsetGet($key) { 21 | if (! $this->offsetExists($key)) $this->loadData($key); 22 | 23 | return parent::offsetGet($key); 24 | } 25 | 26 | 27 | function setDefaultOverride($key, $value) { 28 | if (empty($this['override_defaults'])) $this['override_defaults'] = []; 29 | 30 | $this['override_defaults'][$key] = $value; 31 | 32 | return $this; 33 | } 34 | 35 | 36 | protected function loadData($key) { 37 | $this->offsetSet($key, Yaml::parse(file_get_contents(dirname(__FILE__) . '/../../data/' . $key . '.yml'))); 38 | 39 | return $this; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Helper/Development.php: -------------------------------------------------------------------------------- 1 | config = $config; 17 | } 18 | 19 | 20 | function getTestRequest($req, $with_response = FALSE) { 21 | $built = ReqBuilder::build($req, $this->getSampleRequest($req), new \RoyalMail\Helper\Data()); 22 | 23 | return ($with_response) ? ['request' => $built, 'response' => $this->getSampleRequest($req, 'response')] : $built; 24 | } 25 | 26 | 27 | function getSampleRequest($action, $type = 'request') { 28 | $integration_schema = $this->getTestSchema('requests/integrationHeader'); 29 | $test_schema = $this->getTestSchema('requests/' . $action); 30 | 31 | if ($type == 'response') $type = 'expect'; 32 | 33 | return array_merge($test_schema['valid'][$type], ['integrationHeader' => $integration_schema['valid'][$type]]); 34 | } 35 | 36 | 37 | function getTestSchema($key) { 38 | return Yaml::parse($this->mergeGeneratedValues(file_get_contents(SCHEMA_DIR . '/' . $key . '.yml'))); 39 | } 40 | 41 | 42 | function mergeGeneratedValues($source) { 43 | return preg_replace_callback('/<<([^>]+)>>/', function($matches) { 44 | $parts = explode('|', $matches[1]); 45 | $method = array_shift($parts); 46 | 47 | return (method_exists($this, $method)) 48 | ? call_user_func_array([$this, $method], $parts) 49 | : $matches[0]; 50 | 51 | }, $source); 52 | } 53 | 54 | 55 | function dateVector($interval, $format = 'Y-m-d') { 56 | return '"' . date_create()->modify($interval)->format($format) . '"'; // return quoted string as otherwise the YAML loader seems to be objectifying it. 57 | } 58 | } -------------------------------------------------------------------------------- /src/Helper/Form.php: -------------------------------------------------------------------------------- 1 | getMessage(); 17 | 18 | } catch (\RoyalMail\Exception\RequestException $re) { 19 | foreach ($re->getErrors() as $k_nested => $v) $errors[$k . ':' . $k_nested] = $v; 20 | 21 | } catch (SkipException $e) { return $arr; } // Exception is notification that rules exclude this field. 22 | 23 | 24 | return self::addToArray($arr, $val, $key, @$schema['_key']); 25 | } 26 | 27 | 28 | static function processProperty($schema, $val, $defaults = [], $helper = NULL) { 29 | if (! empty($schema['_skip_blank_tree']) && empty($val)) throw new SkipException('Optional tree of features'); 30 | 31 | switch (TRUE) { 32 | case isset($schema['_include']): return self::processInclude($schema, $val, $defaults, $helper); 33 | case isset($schema['_multiple']): return self::processMultipleProperty($schema, $val, $defaults, $helper); 34 | default: return self::processSingleProperty($schema, $val, $defaults, $helper); 35 | } 36 | } 37 | 38 | 39 | static function processSingleProperty($schema, $val, $defaults = [], $helper = NULL) { 40 | 41 | if ($nested = self::stripMeta($schema)) { 42 | $nest = []; 43 | 44 | foreach ($nested as $k => $v) $nest = self::addProperty($nest, $schema[$k], $k, @$val[$k], $defaults, $helper); 45 | 46 | if (empty($nest)) throw new SkipException('No values nested here'); 47 | 48 | return $nest; 49 | } 50 | 51 | return self::validateAndFilter($schema, $val, $defaults, $helper); 52 | } 53 | 54 | 55 | static function processMultipleProperty($schema, $val, $defaults, $helper = NULL) { 56 | $single_schema = array_diff_key($schema, ['_multiple' => 1, '_key' => 1]); 57 | $multi_schema = $schema['_multiple']; 58 | 59 | if (isset($multi_schema['nest_key'])) $single_schema['_key'] = '~/' . $multi_schema['nest_key']; 60 | 61 | if (is_null($val)) $val = []; // CHECK: required param so always set, but if this is null may not want to be here in the first place. 62 | 63 | $multi_values = []; 64 | 65 | foreach ($val as $m) array_push($multi_values, current(self::addProperty([], $single_schema, '', $m, $defaults, $helper))); 66 | 67 | if (! empty($multi_schema['multi_key'])) { 68 | $multi_values = [$multi_schema['multi_key'] => $multi_values]; 69 | 70 | if (! empty($multi_schema['add_count'])) $multi_values[$multi_schema['add_count']] = count($multi_values[$multi_schema['multi_key']]); 71 | } 72 | 73 | return $multi_values; 74 | } 75 | 76 | 77 | static function processInclude($schema, $val, $defaults, $helper = NULL) { 78 | if (empty($defaults['_disable_includes'])) return self::build($schema['_include'], $val, $helper); 79 | 80 | else throw new SkipException; // Testing is simpler if we can check requests atomically. 81 | } 82 | 83 | 84 | static function validateAndFilter($schema, $val, $defaults, $helper = NULL) { 85 | $schema = array_merge((array) $defaults, $schema); 86 | 87 | $val = self::filter($val, $schema, $type = 'pre', $helper); 88 | 89 | self::validate($schema, $val, $helper); 90 | 91 | return self::filter($val, $schema, $type = 'post', $helper); 92 | } 93 | 94 | 95 | static function addToArray($arr, $val, $key = NULL, $path = NULL) { 96 | if (empty($key)) $key = (int) key(array_slice($arr, -1, 1, TRUE)) + 1; 97 | 98 | if (! empty($path)) { 99 | 100 | $top_ref = & $arr; 101 | 102 | foreach (explode('/', $path) as $step) { // If there is a _key: this/that path value it replaces the $key value entirely. 103 | if ($step === '~') $step = $key; 104 | 105 | if (empty($top_ref[$step])) $top_ref[$step] = []; // New elements can be added to existing paths, so only create what isn't there. 106 | 107 | $top_ref = & $top_ref[$step]; 108 | } 109 | 110 | } else $top_ref = & $arr[$key]; 111 | 112 | $top_ref = $val; 113 | 114 | return $arr; 115 | } 116 | 117 | 118 | static function stripMeta($arr) { 119 | $s = []; 120 | 121 | if (is_array($arr)) foreach ($arr as $k => $v) if (! preg_match('/^_/', $k)) $s[$k] = $v; 122 | 123 | return $s; 124 | } 125 | 126 | 127 | static function derefArray($arr) { 128 | $dereffed = []; 129 | 130 | foreach ($arr as $k => $v) { 131 | $dereffed[$k] = (is_array($v)) ? self::derefArray($v) : $v; 132 | } 133 | 134 | return $dereffed; 135 | } 136 | } -------------------------------------------------------------------------------- /src/Request/Builder.php: -------------------------------------------------------------------------------- 1 | $params] : $helper['input'] = $params; 47 | 48 | $schema['defaults'] = @$schema['defaults'] ?: []; 49 | 50 | if (isset($helper['override_defaults'])) $schema['defaults'] = array_merge($schema['defaults'], $helper['override_defaults']); 51 | 52 | try { 53 | foreach ($schema['properties'] as $k => $v) { 54 | $built = self::addProperty($built, $schema['properties'][$k], $k, @$params[$k], $schema['defaults'], $helper); 55 | } 56 | 57 | } catch (\RoyalMail\Exception\ValidatorException $e) { 58 | $errors[$k] = $k . ': ' . $e->getMessage(); 59 | 60 | } catch (\RoyalMail\Exception\RequestException $re) { 61 | foreach ($re->getErrors() as $k_nested => $v) $errors[$k . ':' . $k_nested] = $v; 62 | } 63 | 64 | if (! empty($errors)) throw (new \RoyalMail\Exception\RequestException())->withErrors($errors); 65 | 66 | if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION > 5) $built = self::derefArray($built); 67 | 68 | return $built; 69 | } 70 | 71 | 72 | static function addProperty($arr, $schema, $key, $val, $defaults = [], $helper = NULL) { 73 | return self::doAddProperty($arr, $schema, $key, $val, $defaults, $helper); 74 | } 75 | 76 | 77 | /** 78 | * Request schemas are kept in YML files. 79 | * These have the structure, validation, and defaults. 80 | * 81 | * @return array 82 | */ 83 | static function getRequestSchema($request_name) { 84 | return Yaml::parse(file_get_contents(dirname(__FILE__) . '/schema/' . $request_name . '.yml')); 85 | } 86 | } -------------------------------------------------------------------------------- /src/Request/schema/cancelShipment.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | cancelShipments: 10 | _key: ~/shipmentNumber 11 | _multiple: { min: 1 } 12 | _help: "The shipment number/s to be cancelled" 13 | _validate: 14 | Length: { max: 13 } -------------------------------------------------------------------------------- /src/Request/schema/createManifest.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | serviceOccurence: 10 | _help: "All or one Service Occurrence. If not included then ALL Service Occurrences are included in the manifest." 11 | _validate: 12 | Range: 13 | min: 0 14 | max: 99 15 | 16 | serviceOffering: 17 | _key: ~/serviceOfferingCode/code 18 | _help: "All or one Service Offerings. If not included then ALL Service Offerings are included in the manifest." 19 | _options: service_offerings 20 | _validate: Choice 21 | 22 | yourDescription: 23 | _help: > 24 | This is a description field that corresponds to the Your Description field in RM’s Online Business Account (OBA). 25 | This is for customer reference and will not appear on any paperwork. 26 | _validate: 27 | Length: { max: 40 } 28 | 29 | yourReference: 30 | _help: "Included on the Customer Collection Receipt." 31 | _validate: 32 | Length: { max: 40 } -------------------------------------------------------------------------------- /src/Request/schema/integrationHeader.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | 4 | 5 | properties: 6 | dateTime: 7 | _help: "This should be populated with the date timestamp when the message was generated." 8 | _hidden: true 9 | _pre_filter: 10 | GenerateDate: 11 | format: 'Y-m-d\TH:i:s' 12 | empty_only: true 13 | 14 | version: 15 | _help: "The version of the API currently being used (currently 2)." 16 | _default: 2 17 | _validate: 18 | Range: { min: 2 } 19 | 20 | applicationId: 21 | _key: "identification/applicationId" 22 | _help: "This is the ten digit Customer Account Number allocated by RM" 23 | _required: true 24 | _validate: 25 | Regex: 26 | pattern: '/^\d{10}$/' 27 | message: 'The Customer Account Number should be exactly 10 digits long.' 28 | 29 | transactionId: 30 | _key: "identification/transactionId" 31 | _help: > 32 | This is a unique number used to identify the transaction as provided by the customer system. 33 | Any value can be provided in this field but must contain only the characters ‘a-z’, ‘A-Z’, ‘0-9’, ‘/’ and ‘-‘. 34 | It allows the consuming application to correlate the response message to its request." 35 | _required: true 36 | _validate: 37 | Regex: 38 | pattern: '/^[a-zA-Z0-9\/-]+$/' 39 | message: 'Your reference ID, should contain only the characters: ‘a-z’, ‘A-Z’, ‘0-9’, ‘/’ and ‘-‘' 40 | 41 | -------------------------------------------------------------------------------- /src/Request/schema/printDocument.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | shipmentNumber: 10 | _help: "Mandatory. The number of the shipment to document." 11 | _required: true 12 | _validate: 13 | Length: { max: 13 } 14 | 15 | documentName: 16 | _help: "Mandatory. The name of the document to output. Valid values are: CN22, CN23 and CI (for Commercial Invoice)." 17 | _required: true 18 | _options: international_documents 19 | _validate: Choice 20 | 21 | documentCopies: 22 | _help: > 23 | Number of copies of the International Document within the single Base64 Encoded PDF document output. 24 | Valid values: 1 or 3 - 3 for Commercial Invoice Only 25 | _options: 26 | '1': 'Single copy' 27 | '3': 'Three copies - only works with the Commercial Invoice option' 28 | _validate: Choice #TODO: validate !3 if ! CI -------------------------------------------------------------------------------- /src/Request/schema/printLabel.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | shipmentNumber: 10 | _help: "The shipment number of the shipment to be printed" 11 | _validate: 12 | Length: { max: 13 } 13 | 14 | outputFormat: 15 | _options: label_formats 16 | _validate: Choice 17 | 18 | localisedAddress: 19 | recipientContact: 20 | name: 21 | _help: "Localised Contact Name." 22 | _validate: 23 | Length: { max: 80 } 24 | 25 | companyName: 26 | _key: complementaryName 27 | _help: "Localised Business Name." 28 | _validate: 29 | Length: { max: 64 } 30 | 31 | 32 | recipientAddress: 33 | addressLine1: 34 | _help: "Localised First line of the address. NOTE: truncated to 35 characters on labels." 35 | _validate: 36 | Length: { max: 80 } 37 | 38 | addressLine2: 39 | _help: "Localised Second line of the address. NOTE: truncated to 35 characters on labels." 40 | _validate: 41 | Length: { max: 80 } 42 | 43 | addressLine3: 44 | _help: "Localised Third line of the address. NOTE: truncated to 35 characters on labels." 45 | _validate: 46 | Length: { max: 80 } 47 | 48 | postTown: 49 | _help: "Localised Town or City. NOTE: truncated to 35 characters on labels." 50 | _validate: 51 | Length: { max: 40 } 52 | 53 | postcode: 54 | _help: "Localised Destination postcode." 55 | _pre_filter: formatGBPostcode 56 | _validate: 57 | - Length: { max: 15 } -------------------------------------------------------------------------------- /src/Request/schema/printManifest.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | manifestBatchNumber: 10 | _help: "This is the batch number to print and is returned by a prior call to createManifest operation." 11 | _validate: 12 | Length: { max: 20 } 13 | 14 | salesOrderNumber: 15 | _help: "The Sales Order Number, which is available via the GUI the day after the manifest was created." 16 | _validate: 17 | Length: { max: 20 } -------------------------------------------------------------------------------- /src/Request/schema/request1DRanges.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader 8 | 9 | services: 10 | _key: serviceReferences 11 | _multiple: 12 | min: 1 13 | multi_key: serviceReference 14 | 15 | # These values are the same as for createShipment. 16 | serviceOccurence: 17 | _help: > 18 | Part of the customer’s contract identifier. In conjunction with the Service Offering it identifies an agreement line on the customer’s account. 19 | If only one Service Reference exists then this is not required. 20 | _validate: 21 | Range: 22 | min: 0 23 | max: 99 24 | 25 | serviceType: 26 | _help: "The system Service Type of the shipment." 27 | _key: ~/code 28 | _options: service_types 29 | _required: true 30 | _validate: Choice 31 | 32 | serviceOffering: 33 | _help: "The Service Offering code for the mail item ordered." 34 | _key: ~/serviceOfferingCode/code 35 | _options: service_offerings 36 | _required: true 37 | _validate: Choice 38 | 39 | signature: 40 | _help: "For RM Tracked items only, this element specifies whether a signature is required on delivery. If this element is not included then it defaults to false." 41 | _pre_filter: Bool 42 | _post_filter: Int 43 | 44 | serviceEnhancements: 45 | _key: ~/enhancementType 46 | _multiple: { nest_key: serviceEnhancementCode/code } 47 | _help: "There can never be more than one enhancementType specified from each Service Enhancement Group." 48 | _options: service_enhancements 49 | _validate: Choice 50 | -------------------------------------------------------------------------------- /src/Request/schema/request2DItemIDRange.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | _trim: true 3 | _skip_blank: true 4 | 5 | properties: 6 | integrationHeader: 7 | _include: integrationHeader -------------------------------------------------------------------------------- /src/Response/Interpreter.php: -------------------------------------------------------------------------------- 1 | loadResponse($key, $response, $helper); 28 | } 29 | 30 | 31 | function succeeded() { return $this->succeeded; } 32 | 33 | function hasIssues() { return $this->hasErrors() || $this->hasWarnings(); } 34 | 35 | function hasErrors() { return count($this->getErrors()) > 0; } 36 | 37 | function getErrors() { return $this->errors; } 38 | 39 | function hasWarnings() { return count($this->getWarnings()) > 0; } 40 | 41 | function getWarnings() { return $this->warnings; } 42 | 43 | function hasDebugInfo() { return is_null($this->debug_info); } 44 | 45 | function hasBinaries() { 46 | foreach ($this->getBinaryKeys() as $bin) if (! empty($this[$bin])) return TRUE; 47 | 48 | return FALSE; 49 | } 50 | 51 | 52 | function getSecurityInfo() { 53 | return $this->security_info; 54 | } 55 | 56 | 57 | function asArray() { return $this->getResponse(); } 58 | function getResponse() { 59 | return $this->getArrayCopy(); 60 | } 61 | 62 | 63 | function getRawResponse() { return $this->response_instance; } 64 | 65 | 66 | function loadResponse($key, $response, $helper = []) { 67 | if ($response instanceof \RoyalMail\Exception\ResponseException) return $this->fail($response); 68 | 69 | $this->response_instance = $response; 70 | $this->response_schema = self::getResponseSchema($key); 71 | 72 | $result = self::build($this->response_schema, $response, $helper); 73 | 74 | if (isset($result['META']['success'])) $this->succeeded = $result['META']['success']; 75 | if (isset($result['META']['security'])) $this->security_info = $result['META']['security']; 76 | if (isset($result['META']['messages']['errors'])) $this->errors = $result['META']['messages']['errors']; 77 | if (isset($result['META']['messages']['warnings'])) $this->warnings = $result['META']['messages']['warnings']; 78 | 79 | if (isset($result['RESPONSE']) && is_array($result['RESPONSE'])) $this->exchangeArray($result['RESPONSE']); 80 | 81 | if ($this->hasErrors()) $this->succeeded = FALSE; 82 | 83 | return $this; 84 | } 85 | 86 | 87 | 88 | static function build($schema, $response, $helper = NULL) { 89 | if (empty($response) && isset($helper['source'])) $response = $helper['source']; 90 | 91 | if (is_scalar($schema)) $schema = self::getResponseSchema($schema); 92 | 93 | return self::processSchema($schema, $response, $helper); 94 | } 95 | 96 | 97 | static function processSchema($schema, $response, $helper = []) { 98 | $built = []; 99 | $helper = is_array($helper) ? array_merge(['source' => $response], $helper) : ['source' => $helper]; 100 | 101 | foreach ($schema['properties'] as $k => $map) { 102 | $built = self::addProperty($built, $map, $k, NULL, [], $helper); 103 | } 104 | 105 | return $built; 106 | } 107 | 108 | 109 | static function addProperty($arr, $schema, $key, $val, $defaults, $helper) { 110 | try { 111 | $val = (empty($schema['_extract'])) ? $val : self::extractValue($helper['source'], $schema); 112 | 113 | } catch (SkipException $e) { 114 | // pass for now - in some circumstances may be best to create an empty structure (defined in schema). 115 | return $arr; 116 | } 117 | 118 | if (! empty($schema['_multiple']) && count($stripped = self::stripMeta($schema))) { 119 | 120 | $schema = array_diff_key($schema, $stripped); // FIXME: This is patching to bypass the default Structure multi property handling 121 | unset($schema['_multiple']); 122 | 123 | $nest = []; 124 | 125 | foreach ($val as $multi) { 126 | $nest[] = self::processSchema(['properties' => $stripped], $multi, array_merge($helper, ['source' => $multi])); 127 | } 128 | 129 | $val = $nest; 130 | } 131 | 132 | return self::doAddProperty($arr, $schema, $key, $val, $defaults, $helper); 133 | } 134 | 135 | 136 | static function extractValue($response, $map) { 137 | foreach (explode('/', $map['_extract']) as $step) { 138 | if (! isset($response->$step)) throw new SkipException('value not present in response'); 139 | 140 | $response = $response->$step; 141 | } 142 | 143 | if (isset($map['_multiple']) && ! is_array($response)) $response = [$response]; // Single entries for multi-optional values in SOAP elide the array. 144 | 145 | return $response; 146 | } 147 | 148 | 149 | function getResponseEncoded() { 150 | $arr = $this->getResponse(); 151 | 152 | if (isset($this->response_schema['binaries'])) foreach (array_keys($this->response_schema['binaries']) as $bin) { 153 | if (! empty($arr[$bin])) $arr[$bin] = base64_encode($arr[$bin]); 154 | } 155 | 156 | return $arr; 157 | } 158 | 159 | 160 | 161 | function getBinaryKeys() { 162 | return array_keys($this->getBinariesInfo()); 163 | } 164 | 165 | 166 | function getBinariesInfo() { 167 | return @$this->response_schema['binaries'] ?: []; 168 | } 169 | 170 | 171 | function getDebugInfo() { return $this->debug_info; } 172 | 173 | function fail($exception) { 174 | $this->errors[] = ['message' => 'API Connection Error: more details available in debug info.']; 175 | 176 | $this->debug_info = $exception; 177 | 178 | return $this; 179 | } 180 | 181 | 182 | function serialise($json_opts = NULL) { 183 | return json_encode([ 184 | 'succeeded' => (int) $this->succeeded(), 185 | 'response' => $this->asArray(), 186 | 'errors' => $this->getErrors(), 187 | 'warnings' => $this->getWarnings(), 188 | 'security' => $this->getSecurityInfo(), 189 | 'debug' => $this->getDebugInfo() 190 | ], $json_opts); 191 | } 192 | 193 | 194 | function __toString() { 195 | return $this->serialise(); 196 | } 197 | 198 | 199 | static function getResponseSchema($response_name) { 200 | return Yaml::parse(file_get_contents(dirname(__FILE__) . '/schema/' . $response_name . '.yml')); 201 | } 202 | } -------------------------------------------------------------------------------- /src/Response/schema/cancelShipment.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: completedCancelInfo/status/status/statusCode/code 5 | _post_filter: { isEqual: 'Cancelled' } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | messages: 11 | _include: integrationFooter 12 | 13 | 14 | RESPONSE: 15 | status: 16 | _extract: completedCancelInfo/status/status/statusCode/code 17 | 18 | updated: 19 | _extract: completedCancelInfo/status/validFrom 20 | _post_filter: ObjectifyDate 21 | 22 | cancelled_shipments: 23 | _extract: completedCancelInfo/completedCancelShipments/shipmentNumber 24 | _multiple: true 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Response/schema/createManifest.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: completedManifests/completedManifestInfo/totalItemCount 5 | _post_filter: { isOver: 0 } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | RESPONSE: 11 | batch_number: 12 | _extract: completedManifests/completedManifestInfo/manifestBatchNumber 13 | 14 | item_count: 15 | _extract: completedManifests/completedManifestInfo/totalItemCount 16 | 17 | shipments: 18 | _extract: completedManifests/completedManifestInfo/manifestShipments/manifestShipment 19 | _multiple: true 20 | 21 | service_offering: 22 | _extract: serviceOffering/serviceOfferingCode/code 23 | 24 | shipment_number: 25 | _extract: shipmentNumber -------------------------------------------------------------------------------- /src/Response/schema/createShipment.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: completedShipmentInfo/status/status/statusCode/code 5 | _post_filter: { isEqual: 'Allocated' } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | messages: 11 | _include: integrationFooter 12 | 13 | 14 | RESPONSE: 15 | status: 16 | _extract: completedShipmentInfo/status/status/statusCode/code 17 | 18 | updated: 19 | _extract: completedShipmentInfo/status/validFrom 20 | _post_filter: ObjectifyDate 21 | 22 | shipments: 23 | _extract: completedShipmentInfo/allCompletedShipments/completedShipments/shipments/shipment 24 | _multiple: true 25 | 26 | number: 27 | _extract: shipmentNumber 28 | 29 | item_id: 30 | _extract: itemID 31 | 32 | status: 33 | _extract: status/status/statusCode/code 34 | 35 | updated: 36 | _extract: status/validFrom 37 | _post_filter: ObjectifyDate 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Response/schema/integrationFooter.yml: -------------------------------------------------------------------------------- 1 | 2 | properties: 3 | errors: 4 | _extract: integrationFooter/errors/error 5 | _multiple: true 6 | 7 | code: 8 | _extract: errorCode 9 | 10 | message: 11 | _extract: errorDescription 12 | 13 | cause: 14 | _extract: errorCause 15 | 16 | fix: 17 | _extract: errorResolution 18 | 19 | context: 20 | _extract: errorContext 21 | 22 | 23 | warnings: 24 | _extract: integrationFooter/warnings/warning 25 | _multiple: true 26 | 27 | code: 28 | _extract: warningCode 29 | 30 | message: 31 | _extract: warningDescription 32 | 33 | cause: 34 | _extract: warningCause 35 | 36 | fix: 37 | _extract: warningResolution 38 | 39 | context: 40 | _extract: warningContext -------------------------------------------------------------------------------- /src/Response/schema/integrationHeader.yml: -------------------------------------------------------------------------------- 1 | 2 | properties: 3 | timestamp: 4 | _extract: integrationHeader/dateTime 5 | _post_filter: ObjectifyDate 6 | 7 | version: 8 | _extract: integrationHeader/version 9 | 10 | application_id: 11 | _extract: integrationHeader/identification/applicationId 12 | 13 | transaction_id: 14 | _extract: integrationHeader/identification/transactionId 15 | 16 | -------------------------------------------------------------------------------- /src/Response/schema/printDocument.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: integrationHeader/version 5 | _post_filter: { isEqual: 2 } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | messages: 11 | _include: integrationFooter 12 | 13 | RESPONSE: 14 | document: 15 | _extract: internationalDocument 16 | 17 | 18 | binaries: 19 | document: application/pdf -------------------------------------------------------------------------------- /src/Response/schema/printLabel.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: outputFormat 5 | _post_filter: Bool 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | messages: 11 | _include: integrationFooter 12 | 13 | RESPONSE: 14 | label: 15 | _extract: label 16 | 17 | 1DBarcode: 18 | _extract: labelImages/image1DBarcode 19 | 20 | 2DBarcode: 21 | _extract: labelImages/image2DBarcode 22 | 23 | format: 24 | _extract: outputFormat 25 | 26 | 27 | binaries: 28 | label: application/pdf 29 | 30 | 1DBarcode: image/png 31 | 32 | 2DBarcode: image/png 33 | -------------------------------------------------------------------------------- /src/Response/schema/printManifest.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: integrationHeader/version 5 | _post_filter: { isEqual: 2 } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | messages: 11 | _include: integrationFooter 12 | 13 | RESPONSE: 14 | manifest: 15 | _extract: manifest 16 | 17 | 18 | binaries: 19 | manifest: application/pdf -------------------------------------------------------------------------------- /src/Response/schema/request1DRanges.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: integrationHeader/version 5 | 6 | security: 7 | _include: integrationHeader 8 | 9 | RESPONSE: 10 | ranges: 11 | _extract: serviceRanges/serviceRange 12 | _multiple: true 13 | 14 | service_occurence: 15 | _extract: serviceReference/serviceOccurrence 16 | 17 | service_offering: 18 | _extract: serviceReference/serviceOffering/serviceOfferingCode/code 19 | 20 | enhancements: 21 | _extract: serviceReference/serviceEnhancements/enhancementType/serviceEnhancementCode/code 22 | _multiple: true 23 | 24 | signature: 25 | _extract: serviceReference/signature 26 | 27 | service_type: 28 | _extract: serviceReference/serviceType/code 29 | 30 | start: 31 | _extract: barcode1DRange/barcode1DRangeStart 32 | 33 | end: 34 | _extract: barcode1DRange/barcode1DRangeEnd 35 | -------------------------------------------------------------------------------- /src/Response/schema/request2DItemIDRange.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | META: 3 | success: 4 | _extract: itemIDRange/itemIDRangeEnd 5 | _post_filter: { isOver: 0 } 6 | 7 | security: 8 | _include: integrationHeader 9 | 10 | RESPONSE: 11 | start: 12 | _extract: itemIDRange/itemIDRangeStart 13 | 14 | end: 15 | _extract: itemIDRange/itemIDRangeEnd 16 | -------------------------------------------------------------------------------- /src/RoyalMail.php: -------------------------------------------------------------------------------- 1 | TRUE, 27 | 'timezone' => 'UTC', 28 | 'username' => NULL, 29 | 'password' => NULL, 30 | 'application_id' => NULL, 31 | 'transaction_id' => NULL, 32 | 'mode' => 'development', 33 | 'request_schema' => 'src/Request/schema', 34 | 'response_schema' => 'src/Response/schema', 35 | 'endpoint' => '', 36 | 'soap_client_options' => [ 37 | 'local_cert' => NULL, 38 | 'trace' => 1, 39 | 'location' => '' 40 | ], 41 | ], 42 | 43 | $modes = [ 44 | 'development' => ['soap_client' => STATIC_CLIENT, 'endpoint' => STATIC_ENDPOINT, 'static_responses' => STATIC_RESPONSE_DIRECTORY], 45 | 'onboarding' => ['endpoint' => STATIC_ENDPOINT, 'soap_client_options' => ['location' => 'https://api.royalmail.net/shipping/v2']], 46 | 'live' => ['endpoint' => STATIC_ENDPOINT, 'soap_client_options' => ['location' => 'https://api.royalmail.com/shipping/v2']] 47 | ]; 48 | 49 | 50 | 51 | /** 52 | * Create New 53 | * 54 | * @param array $args This should contain security details and config default overrides. 55 | * 56 | */ 57 | function __construct($args = []) { 58 | $this->configure($args); 59 | } 60 | 61 | 62 | function processAction($action, $params, $config = []) { 63 | if (empty($params['integrationHeader'])) $params = $this->addIntegrationHeader($params); 64 | 65 | return $this->interpretResponse($action, 66 | $this->send($action, 67 | $this->buildRequest($action, $params, $config) 68 | ) 69 | ); 70 | } 71 | 72 | 73 | function buildRequest($action, $params, $config = []) { 74 | return Builder::build($action, $params, $this->getDataHelper($config)); 75 | } 76 | 77 | 78 | function send($action, $request, $config = []) { 79 | try { 80 | return $this->getConnector()->request($action, $request, $config); 81 | 82 | } catch (\SoapFault $e) { 83 | return new \RoyalMail\Exception\ResponseException(array_merge(['exception' => $e], $this->getConnector()->getDebugInfo())); 84 | } 85 | } 86 | 87 | 88 | function interpretResponse($action, $response) { 89 | return new Interpreter($action, $response, []); 90 | } 91 | 92 | 93 | function addIntegrationHeader($params) { 94 | $params['integrationHeader'] = [ 95 | 'applicationId' => $this->getConfig()['application_id'], 96 | 'transactionId' => $this->getConfig()['transaction_id'], 97 | ]; 98 | 99 | if (! empty($params['transaction_id'])) { 100 | $params['integrationHeader']['transactionId'] = $params['transaction_id']; 101 | unset($params['transaction_id']); 102 | } 103 | 104 | return $params; 105 | } 106 | 107 | 108 | /** 109 | * Get the appropriate connector class 110 | * 111 | * @return \RoyalMail\Connector\baseConnector Variation on... 112 | */ 113 | function getConnector() { 114 | if (empty($this->connector)) $this->connector = new Connector($this->config); 115 | 116 | return $this->connector; 117 | } 118 | 119 | 120 | function getDataHelper($config = []) { 121 | if (empty($this->data_helper)) $this->data_helper = new Store($config); 122 | 123 | return $this->data_helper; 124 | } 125 | 126 | 127 | /** 128 | * Set up config values, these are merged with the defaults. 129 | * 130 | * @param array 131 | * 132 | * @return RoyalMail\RoyalMail $this 133 | */ 134 | function configure($config = []) { 135 | $this->config = array_replace_recursive($this->config, $this->modes[@$config['mode'] ?: $this->config['mode']]); 136 | 137 | $this->config = array_replace_recursive($this->config, $config); 138 | 139 | return $this; 140 | } 141 | 142 | 143 | function getConfig($key = NULL) { 144 | return (empty($key)) ? $this->config : $this->config[$key]; 145 | } 146 | 147 | 148 | function getAvailableActions() { 149 | $actions = []; 150 | 151 | foreach (glob(MODULE_ROOT . $this->getConfig('request_schema') . '/*.yml') as $schema) { 152 | $actions[] = basename($schema, '.yml'); 153 | } 154 | 155 | return $actions; 156 | } 157 | 158 | 159 | function getDevelopmentHelper() { 160 | if (empty($this->dev_helper)) $this->dev_helper = new DevHelper($this->getConfig()); 161 | 162 | return $this->dev_helper; 163 | } 164 | 165 | 166 | function __call($method, $args) { 167 | if (in_array($method, $this->getAvailableActions())) { 168 | return call_user_func_array([$this, 'processAction'], array_merge([$method], $args)); 169 | } 170 | 171 | throw new \BadMethodCallException('Action "' . $method . '"" not configured.'); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Validator/Validates.php: -------------------------------------------------------------------------------- 1 | []]; // Shorthand version for single named validator e.g. _validate: NotBlank 48 | if (count($constraints) && ! isset($constraints[0])) $constraints = [$constraints]; // Shorthand version for single validator with options. e.g. _validate: { Length: 20 } 49 | 50 | if (! empty($schema['_required'])) array_unshift($constraints, ['NotBlank' => TRUE]); // Shorthand required values, always first test. 51 | 52 | return $constraints; 53 | } 54 | 55 | 56 | 57 | /** 58 | * Adds extra settings values from the schema as they may be used by the validator as well as other things. 59 | * 60 | * @param mixed $params 61 | * @param array $schema 62 | * 63 | * @return array 64 | */ 65 | static function parseParams($params, $schema) { 66 | $params = (array) $params; 67 | 68 | foreach (array_diff_key($schema, ['_validate' => 1]) as $k => $v) if (preg_match('/^_/', $k)) $params[$k] = $v; 69 | 70 | return $params; 71 | } 72 | 73 | 74 | 75 | /** 76 | * Apply the given constraints with the parameters given. 77 | * 78 | * @param mixed $value 79 | * @param string $constraint 80 | * @param array $params 81 | * 82 | * @return mixed 83 | */ 84 | static function constrain($value, $constraint, $params = [], $helper = NULL) { 85 | $constraint_method = get_called_class() . '::check' . $constraint; 86 | 87 | if (! is_callable($constraint_method)) throw new \InvalidArgumentException('Invalid constraint method ' . $constraint_method . ' called'); 88 | 89 | return call_user_func_array($constraint_method, [$value, (array) $params, $helper]); 90 | } 91 | 92 | 93 | 94 | /** 95 | * NotBlank 96 | * 97 | */ 98 | static function checkNotBlank($value, $params, $helper) { 99 | if (! self::isBlank($value)) return $value; 100 | 101 | self::fail($value, $params, ['message' => 'can not be blank']); 102 | } 103 | 104 | 105 | 106 | static function checkRange($value, $params, $helper) { 107 | if (! is_numeric($value)) { 108 | self::fail($value, $params, ['message' => 'numeric value required']); 109 | } 110 | 111 | if (isset($params['min']) && ($value < $params['min'])) { 112 | self::fail($value, $params, ['message' => @$params['min_message'] ?: 'value should be over or equal to ' . $params['min']]); 113 | } 114 | 115 | if (isset($params['max']) && ($value > $params['max'])) { 116 | self::fail($value, $params, ['message' => @$params['max_message'] ?: 'value should be under or equal to ' . $params['min']]); 117 | } 118 | 119 | return $value; 120 | } 121 | 122 | 123 | 124 | static function checkRegex($value, $params, $helper) { 125 | if (! preg_match($params['pattern'], $value)) self::fail($value, $params, ['message' => $params['pattern'] . ' regex not matched']); 126 | 127 | return $value; 128 | } 129 | 130 | 131 | 132 | static function checkChoice($value, $params, $helper) { 133 | $choices = isset($params['choices']) ? $params['choices'] : $params['_options']; 134 | 135 | if (is_scalar($choices)) $choices = array_keys($helper[$choices]); 136 | 137 | if (! in_array($value, $choices)) self::fail($value, $params, ['message' => 'accepted values are ' . implode(', ', $choices)]); 138 | 139 | return $value; 140 | } 141 | 142 | 143 | 144 | static function checkDate($value, $params, $helper) { 145 | $original = $value; 146 | 147 | if (! $value instanceof \DateTime) try { 148 | $value = (empty($params['format'])) ? date_create($value) : date_create_from_format($params['format'], $value); 149 | 150 | } catch (Exception $e) { 151 | self::fail($value, $params, ['message' => 'value not in a valid date format.']); 152 | } 153 | 154 | if (isset($params['min']) && $value < date_create()->setTime(00, 00, 01)->modify($params['min'])) { 155 | self::fail($value->format('Y-m-d'), $params, ['message' => 'date earlier than ' . date_create()->modify($params['min'])->format('Y-m-d') . ' | now ' . $params['min']]); // TODO: mebbe possible to display date in i18n format. 156 | } 157 | 158 | if (isset($params['max']) && $value > date_create()->modify($params['max'])->setTime(23, 59, 59)) { 159 | self::fail($value->format('Y-m-d'), $params, ['message' => 'date later than now ' . $params['max']]); 160 | } 161 | 162 | return $original; 163 | } 164 | 165 | 166 | static function checkLength($value, $params, $helper) { 167 | $func = function_exists('mb_strlen') 168 | ? function ($str) { return mb_strlen($str); } 169 | : function ($str) { return strlen($str); }; 170 | 171 | if (isset($params['min']) && $func($value) < $params['min']) { 172 | self::fail($value, $params, ['message' => 'value should be at least ' . $params['min'] . ' characters long']); 173 | } 174 | 175 | if (isset($params['max']) && $func($value) > $params['max']) { 176 | self::fail($value, $params, ['message' => 'value should be at least ' . $params['max'] . ' characters long']); 177 | } 178 | 179 | return $value; 180 | } 181 | 182 | 183 | static function checkNumeric($value, $params, $helper) { 184 | if (! is_numeric($value)) { 185 | self::fail($value, $params, ['message' => 'value contains non-numeric characters']); 186 | } 187 | 188 | return $value; 189 | } 190 | 191 | 192 | static function checkEmail($value, $params, $helper) { 193 | if (filter_var($value, \FILTER_VALIDATE_EMAIL) === FALSE) { 194 | self::fail($value, $params, ['message' => 'not a valid email']); 195 | } 196 | 197 | return $value; 198 | } 199 | 200 | 201 | static function checkGBPostcode($value, $params, $helper) { 202 | if (! (isset($params['check_country'])) || self::checkPath($params['check_country'], ['in' => ['GB']], $helper)) { 203 | 204 | if (! preg_match('/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/', $value)) { 205 | self::fail($value, $params, ['message' => 'not a valid UK postcode']); 206 | } 207 | 208 | } 209 | 210 | return $value; 211 | } 212 | 213 | 214 | /** 215 | * Check field exists based on the value of another field. 216 | * 217 | * NB: Valitron's equals method checks against a field val, not a string, so 'in' is used for strings and arrays. 218 | */ 219 | static function checkThisRequiredWhenThat($value, $params, $helper) { 220 | if (self::checkPath($params['that'], ['required' => TRUE, 'in' => (array) $params['is']], $helper)) { 221 | $params = array_merge(['message' => 'required when ' . $params['that'] . ' in (' . implode(', ', (array) $params['is']) . ')'], $params); 222 | 223 | return self::checkNotBlank($value, $params, $helper); 224 | } 225 | 226 | return $value; 227 | } 228 | 229 | 230 | static function isBlank($value) { 231 | return ($value === FALSE || $value === '' || $value === NULL); 232 | } 233 | 234 | 235 | static function hasValue($arr, $field) { 236 | return self::is($arr, ['required' => $field]); 237 | } 238 | 239 | 240 | /** 241 | * Convert the validation from the schema with has a 242 | * -- Field => [Validation settings, /...] 243 | * layout to 244 | * -- Validation Rule => [[Field, Settings], [], ... ] 245 | * 246 | * Need to add custom rules for checks not covered by the default Valitron rules (i.e. ThisRequiredWhenThat()). 247 | */ 248 | static function buildValitronRules($schema) { 249 | return []; 250 | } 251 | 252 | 253 | /** 254 | * Build a Valitron ruleset from a given path and constraints. 255 | * 256 | * @param string $path (input|output):dot.path.to.var.*.vars 257 | * @param array $constraints constraints and other settings, defaults to ['required' => TRUE] if empty 258 | * @param $helper ArrayObject data checks are run on. 259 | * 260 | * @return Boolean True if rules validate, False otherwise. 261 | */ 262 | static function checkPath($path, $constraints, $helper) { 263 | list($where, $path) = explode(':', $path); 264 | 265 | if (empty($constraints)) $constraints = ['required' => TRUE]; 266 | 267 | foreach ($constraints as $con => $params) { 268 | if ($con === 'required') $constraints['required'] = $path; 269 | 270 | elseif ($con === 'in') $constraints['in'] = [[$path, $params]]; // !REM!: params for Valitron rule in indexed array. 271 | 272 | else unset($constraints[$con]); 273 | } 274 | 275 | 276 | return self::is($helper[$where], $constraints); 277 | } 278 | 279 | 280 | static function is($arr, $rules) { 281 | return ! is_array(self::callValitron($arr, $rules, $throw = FALSE)); 282 | } 283 | 284 | 285 | static function callValitron($arr, $rules, $throw = TRUE) { 286 | $v = new Validator($arr); 287 | 288 | $v->rules($rules); 289 | 290 | if (! $v->validate()) { 291 | if ($throw) throw (new \RoyalMail\Exception\RequestException())->withErrors($v->errors()); 292 | 293 | return $v->errors(); 294 | 295 | } else return TRUE; 296 | } 297 | 298 | 299 | /** 300 | * Handle failed validation constraint. 301 | * 302 | * @param mixed $value 303 | * @param array $params 304 | * @param array $defaults 305 | * 306 | * @throws \RoyalMail\Exception\ValidatorException 307 | */ 308 | static function fail($value, $params, $defaults = []) { 309 | $params = array_merge(['message' => 'value is invalid'], $defaults, $params); 310 | $show_val = is_scalar($value) ? ' [' . $value . ']' : ''; 311 | 312 | throw new \RoyalMail\Exception\ValidatorException($params['message'] . $show_val); 313 | } 314 | } -------------------------------------------------------------------------------- /tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | these should all be able to run without connecting with the actaul API as they won't have access to auth details. 4 | 5 | -------------------------------------------------------------------------------- /tests/lib/TestDataLoader.php: -------------------------------------------------------------------------------- 1 | 'blah', 24 | 'username' => 'blah', 25 | 'timezone' => 'BST', 26 | 'trace' => 1, 27 | 'static_responses' => MODULE_ROOT . 'reference/responses' 28 | ]); 29 | } 30 | 31 | 32 | function getDevelopmentHelper($config = []) { 33 | if (empty($this->development_helper)) $this->development_helper = new \RoyalMail\Helper\Development($config); 34 | 35 | return $this->development_helper; 36 | } 37 | 38 | 39 | function __call($method, array $args) { 40 | if (method_exists($this->getDevelopmentHelper(), $method)) return call_user_func_array([$this->getDevelopmentHelper(), $method], $args); 41 | 42 | return parent::__call($method, $args); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/onboarding/README: -------------------------------------------------------------------------------- 1 | Tests to run to verify system is compliant prior to going live. 2 | 3 | -------------------------------------------------------------------------------- /tests/resources/misc_builder_tests.yml: -------------------------------------------------------------------------------- 1 | 2 | multiple_property_single_element: 3 | schema: 4 | properties: 5 | multiprop: 6 | _key: ~/parent_path 7 | _multiple: true 8 | 9 | values: 10 | multiprop: 11 | - one 12 | - two 13 | - three 14 | - banana 15 | - four 16 | 17 | 18 | expect: 19 | multiprop: 20 | parent_path: 21 | - one 22 | - two 23 | - three 24 | - banana 25 | - four 26 | 27 | 28 | weight_from_create_shipment: 29 | schema: 30 | properties: 31 | weight: 32 | grams: 33 | _key: value 34 | _help: > 35 | Weight in grams of each item (no decimal places). If the service has a weight band, for example Special Delivery, 36 | then the upper band will be used. For example, 150 grams will use the 100 to 200 grams band. Tracked services, for 37 | example, do not have a band and therefore take the actual weight. 38 | _pre_filter: Round 39 | _validate: 40 | Range: 41 | min: 0 42 | max: 99999 43 | units: 44 | _help: "Must be ‘g’ for grams" 45 | _key: unitOfMeasure/unitOfMeasureCode/code 46 | _default: "g" 47 | _hidden: true 48 | 49 | values: 50 | weight: 51 | grams: 100 52 | unitOfMeasure: 53 | unitOfMeasureCode: g 54 | 55 | 56 | expect: 57 | weight: 58 | value: 100 59 | unitOfMeasure: 60 | unitOfMeasureCode: 61 | code: g 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/resources/requests/cancelShipment.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | cancelShipments: 4 | - "RQ221150289GB" 5 | - "RQ221150290GB" 6 | 7 | 8 | expect: 9 | cancelShipments: 10 | shipmentNumber: [ "RQ221150289GB", "RQ221150290GB" ] -------------------------------------------------------------------------------- /tests/resources/requests/createManifest.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | serviceOccurence: "2" 4 | serviceOffering: "TRM" 5 | yourDescription: "Optional description, not shown on paperwork..." 6 | yourReference: "Optional reference, included on customer collection receipt" 7 | 8 | expect: 9 | serviceOccurence: "2" 10 | serviceOffering: 11 | serviceOfferingCode: { code: "TRM" } 12 | yourDescription: "Optional description, not shown on paperwork..." 13 | yourReference: "Optional reference, included on customer collection receipt" -------------------------------------------------------------------------------- /tests/resources/requests/createShipment.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | requestedShipment: 4 | shipmentType: 'Delivery' 5 | serviceOccurrence: 1 6 | serviceType: T 7 | serviceOffering: TRM 8 | serviceFormat: P.int 9 | shippingDate: <> 10 | bfpoFormat: FAE 11 | signature: true 12 | customerReference: CustSuppRef1 13 | senderReference: SenderReference1 14 | safePlace: in the garage 15 | 16 | serviceEnhancements: # Need to find out the actual format multiples with nested items need to be in. 17 | - "5" 18 | - "13" 19 | 20 | recipientContact: 21 | name: Mr Tom Smith 22 | companyName: Department 98 23 | phone: "07801 123 456" 24 | email: tom.smith@royalmail.com 25 | 26 | recipientAddress: 27 | addressLine1: 44-46 Morningside Road 28 | postTown: Edinburgh 29 | postcode: Eh10 4Bf 30 | country: GB 31 | items: 32 | - 33 | numberOfItems: "2" 34 | weight: 35 | grams: 100 36 | 37 | expect: 38 | requestedShipment: 39 | shipmentType: 40 | code: 'Delivery' 41 | 42 | serviceOccurrence: 1 43 | 44 | serviceType: 45 | code: T 46 | 47 | serviceOffering: 48 | serviceOfferingCode: 49 | code: TRM 50 | 51 | serviceFormat: 52 | serviceFormatCode: 53 | code: P 54 | 55 | shippingDate: <> 56 | 57 | bfpoFormat: 58 | bFPOFormatCode: 59 | code: FAE 60 | signature: 1 61 | 62 | customerReference: CustSuppRef1 63 | senderReference: SenderReference1 64 | safePlace: in the garage 65 | 66 | 67 | serviceEnhancements: 68 | enhancementType: 69 | - serviceEnhancementCode: { code: "5" } 70 | - serviceEnhancementCode: { code: "13" } 71 | 72 | 73 | recipientContact: 74 | name: Mr Tom Smith 75 | complementaryName: Department 98 76 | telephoneNumber: 77 | countryCode: "0044" 78 | telephoneNumber: "07801123456" 79 | electronicAddress: 80 | electronicAddress: tom.smith@royalmail.com 81 | 82 | recipientAddress: 83 | addressLine1: 44-46 Morningside Road 84 | postTown: Edinburgh 85 | postcode: EH10 4BF 86 | country: 87 | countryCode: 88 | code: GB 89 | 90 | items: 91 | 92 | item: 93 | - 94 | numberOfItems: "2" 95 | weight: 96 | value: 100 97 | unitOfMeasure: 98 | unitOfMeasureCode: 99 | code: g 100 | 101 | # offlineShipments: 102 | # - 103 | # itemID: 2000001 104 | # status: 105 | # statusCode: null 106 | # validFrom: 2015-02-09T09:52:06.000+02:00 107 | # - 108 | # itemID: 2000002 109 | # status: 110 | # statusCode: null 111 | # validFrom: 2015-02-09T09:52:06.000+02:00 112 | 113 | minimal: 114 | requestedShipment: 115 | shipmentType: 'Delivery' 116 | serviceType: T 117 | serviceOffering: TRM 118 | 119 | recipientContact: 120 | name: Mr Tom Smith 121 | 122 | recipientAddress: 123 | addressLine1: 44-46 Morningside Road 124 | postTown: Edinburgh 125 | postcode: Eh10 4Bf 126 | -------------------------------------------------------------------------------- /tests/resources/requests/createShipment_International.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | requestedShipment: 4 | shipmentType: 'Delivery' 5 | serviceOccurrence: 1 6 | serviceType: T 7 | serviceOffering: TRM 8 | serviceFormat: P.int 9 | shippingDate: <> 10 | bfpoFormat: FAE 11 | signature: true 12 | customerReference: CustSuppRef1 13 | senderReference: SenderReference1 14 | safePlace: in the garage 15 | 16 | serviceEnhancements: # Need to find out the actual format multiples with nested items need to be in. 17 | - "5" 18 | - "13" 19 | 20 | recipientContact: 21 | name: Mr Tom Smith 22 | companyName: Department 98 23 | phone: "07801 123 456" 24 | email: tom.smith@royalmail.com 25 | 26 | recipientAddress: 27 | addressLine1: 44-46 Morningside Road 28 | postTown: Edinburgh 29 | postcode: Eh10 4Bf 30 | country: GB 31 | items: 32 | - 33 | numberOfItems: "2" 34 | weight: 35 | grams: 100 36 | 37 | internationalInfo: 38 | parcels: 39 | - 40 | weight: 1.2 41 | length: 22 42 | width: 22 43 | height: 10 44 | purposeOfShipment: 22 45 | explanation: description here 46 | invoiceNumber: ABC123 47 | 48 | contentDetail: 49 | countryOfManufacture: GB 50 | manufacturersName: ACME Ltd. 51 | description: roadrunner trap 52 | unitWeight: 53 | value: 1.2 54 | unitQuantity: 1 55 | currencyCode: GBP 56 | tariffCode: 123 57 | tariffDescription: tariff description 58 | 59 | 60 | expect: 61 | requestedShipment: 62 | shipmentType: 63 | code: 'Delivery' 64 | 65 | serviceOccurrence: 1 66 | 67 | serviceType: 68 | code: T 69 | 70 | serviceOffering: 71 | serviceOfferingCode: 72 | code: TRM 73 | 74 | serviceFormat: 75 | serviceFormatCode: 76 | code: P 77 | 78 | shippingDate: <> 79 | 80 | bfpoFormat: 81 | bFPOFormatCode: 82 | code: FAE 83 | signature: 1 84 | 85 | customerReference: CustSuppRef1 86 | senderReference: SenderReference1 87 | safePlace: in the garage 88 | 89 | 90 | serviceEnhancements: 91 | enhancementType: 92 | - serviceEnhancementCode: { code: "5" } 93 | - serviceEnhancementCode: { code: "13" } 94 | 95 | 96 | recipientContact: 97 | name: Mr Tom Smith 98 | complementaryName: Department 98 99 | telephoneNumber: 100 | countryCode: "0044" 101 | telephoneNumber: "07801123456" 102 | electronicAddress: 103 | electronicAddress: tom.smith@royalmail.com 104 | 105 | recipientAddress: 106 | addressLine1: 44-46 Morningside Road 107 | postTown: Edinburgh 108 | postcode: EH10 4BF 109 | country: 110 | countryCode: 111 | code: GB 112 | 113 | items: 114 | 115 | item: 116 | - 117 | numberOfItems: "2" 118 | weight: 119 | value: 100 120 | unitOfMeasure: 121 | unitOfMeasureCode: 122 | code: g 123 | 124 | internationalInfo: 125 | parcels: 126 | parcel: 127 | - 128 | weight: 1.2 129 | length: 22 130 | width: 22 131 | height: 10 132 | purposeOfShipment: "22" 133 | explanation: description here 134 | invoiceNumber: ABC123 135 | 136 | contentDetail: 137 | countryOfManufacture: 138 | countryCode: 139 | code: GB 140 | manufacturersName: ACME Ltd. 141 | description: roadrunner trap 142 | unitWeight: 143 | unitOfMeasure: 144 | unitOfMeasureCode: 145 | code: KG 146 | value: 1.2 147 | 148 | unitQuantity: 1 149 | currencyCode: 150 | code: GBP 151 | tariffCode: 152 | code: "123" 153 | tariffDescription: tariff description 154 | -------------------------------------------------------------------------------- /tests/resources/requests/integrationHeader.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | dateTime: "2015-10-14T17:14:32" 4 | applicationId: ' 0123456789' # If this isn't quoted it gets changed to 342391, not octal, not hex, just weird. TODO: find out why. 5 | transactionId: 'order-123' 6 | 7 | expect: 8 | dateTime: "2015-10-14T17:14:32" 9 | version: 2 10 | identification: 11 | applicationId: '0123456789' 12 | transactionId: 'order-123' -------------------------------------------------------------------------------- /tests/resources/requests/printDocument.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | shipmentNumber: HY188980152GB 4 | documentName: CN22 5 | documentCopies: 1 6 | 7 | 8 | expect: 9 | shipmentNumber: HY188980152GB 10 | documentName: CN22 11 | documentCopies: 1 -------------------------------------------------------------------------------- /tests/resources/requests/printLabel.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | shipmentNumber: HY188980152GB 4 | 5 | 6 | expect: 7 | shipmentNumber: HY188980152GB -------------------------------------------------------------------------------- /tests/resources/requests/printManifest.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | manifestBatchNumber: HY188980152GB # In reality only one value would be used. 4 | salesOrderNumber: 12345 5 | 6 | 7 | expect: 8 | manifestBatchNumber: HY188980152GB 9 | salesOrderNumber: 12345 10 | 11 | -------------------------------------------------------------------------------- /tests/resources/requests/request1DRanges.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | services: 4 | - 5 | serviceOccurence: 1 6 | serviceType: T 7 | serviceOffering: TRM 8 | signature: true 9 | serviceEnhancements: 10 | - "5" 11 | - "13" 12 | 13 | expect: 14 | serviceReferences: 15 | serviceReference: 16 | - 17 | serviceOccurence: 1 18 | 19 | serviceType: 20 | code: T 21 | 22 | serviceOffering: 23 | serviceOfferingCode: 24 | code: TRM 25 | 26 | signature: true 27 | 28 | serviceEnhancements: 29 | enhancementType: 30 | - serviceEnhancementCode: { code: "5" } 31 | - serviceEnhancementCode: { code: "13" } 32 | -------------------------------------------------------------------------------- /tests/resources/requests/request2DItemIDRange.yml: -------------------------------------------------------------------------------- 1 | # Single command request, only integrationHeader sent. 2 | 3 | valid: 4 | request: [] 5 | 6 | expect: [] 7 | -------------------------------------------------------------------------------- /tests/resources/responses/cancelShipment.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:35:28' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '420642961' 6 | 7 | response: 8 | status: 'Cancelled' 9 | updated: '2015-02-09T10:35:28.000+02:00' 10 | cancelled_shipments: ['RQ221150275GB'] -------------------------------------------------------------------------------- /tests/resources/responses/createManifest.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:16:56' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '102122313' 6 | 7 | response: 8 | batch_number: '81' 9 | item_count: '2' 10 | shipments: 11 | - { service_offering: 'MP6', shipment_number: 'RQ221150275GB' } 12 | - { service_offering: 'TRM', shipment_number: 'HY188980152GB' } -------------------------------------------------------------------------------- /tests/resources/responses/createShipment.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T08:52:03' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '730222611' 6 | 7 | response: 8 | status: 'Allocated' 9 | updated: '2015-02-09T09:52:06.000+02:00' 10 | 11 | shipments: 12 | - 13 | number: 'HY188980152GB' 14 | item_id: '1000076' 15 | status: 'Allocated' 16 | updated: '2015-02-09T09:52:06.000+02:00' 17 | - 18 | number: 'HY188980166GB' 19 | item_id: '1000077' 20 | status: 'Allocated' 21 | updated: '2015-02-09T09:52:06.000+02:00' 22 | 23 | 24 | has_warnings: true -------------------------------------------------------------------------------- /tests/resources/responses/printDocument.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:14:32' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '667767512' 6 | 7 | response: 8 | document: "JVBERi0xLjMKJeLjz9MKMSAwIG9iajw8L1Byb2R1Y2VyKGh0bWxkb2MgMS44LjI3IENvcHlyaWdodCAxOTk3LTIwMDYgRWFzeSBTb2Z0d2FyZSBQcm9kdWN0cywgQWxsIFJpZ2h0cyBSZXNlcnZlZC4pL0NyZWF0aW9uRGF0ZShEOjIwMTUwMjA5MTAxNDMyLTAxMDApPj5lbmRvYmoKMiAwIG9iajw8L1R5cGUvRW5jb2RpbmcvRGlmZmVyZW5jZXNbIDMyL3NwYWNlL2V4Y2xhbS9xdW90ZWRibC9udW1iZXJzaWduL2RvbGxhci9wZXJjZW50L2FtcGVyc2FuZC9xdW90ZXNpbmdsZS9wYXJlbmxlZnQvcGFyZW5yaWdodC9hc3Rlcmlzay9wbHVzL2NvbW1hL2h5cGhlbi9wZXJpb2Qvc2xhc2gvemVyby9vbmUvdHdvL3RocmVlL2ZvdXIvZml2ZS9zaXgvc2V2ZW4vZWlnaHQvbmluZS9jb2xvbi9zZW1pY29sb24vbGVzCnRyYWlsZXIKPDwvU2l6ZSAyNi9Sb290IDI1IDAgUi9JbmZvIDEgMCBSL0lEWzw1Nzc0ODI2ZGMxMDI3ZmIzODgwZGMyMDE3YjQ5MGI2OT48NTc3NDgyNmRjMTAyN2ZiMzg4MGRjMjAxN2I0OTBiNjk+XT4+CnN0YXJ0eHJlZgoxNzEzMjUKJSVFT0YK" 9 | 10 | has_warnings: true -------------------------------------------------------------------------------- /tests/resources/responses/printLabel.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:03:19' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '467089462' 6 | 7 | response: #YAML multiline adding spaces to label, so all on one line now. 8 | label: "JVBERi0xLjYKJeTjz9IKMSAwIG9iagpbL1BERi9JbWFnZUIvSW1hZ2VDL0ltYWdlSS9UZXh0XQplbmRvYmoKNCAwIG9iago8PC9MZW5ndGggNSAwIFIKL0ZpbHRlci9GbGF0ZURlY29kZQo+PgpzdHJlYW0KeJwDAAAAAAEKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjgKZW5kb2JqCjYgMCBvYmoKPDwv+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6SlpqeoMCBSCi9JbmZvIDMyIDAgUgovSURbPDZDM0VCNEREOEE2OTNEMTVDQUE4NkRCODJCNTc2MTIzPjw2QzNFQjRERDhBNjkzRDE1Q0FBODZEQjgyQjU3NjEyMz5dCj4+CnN0YXJ0eHJlZgoxMzI1OTYKJSVFT0YK" 9 | format: PDF -------------------------------------------------------------------------------- /tests/resources/responses/printManifest.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:19:19' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '335379752' 6 | 7 | response: 8 | manifest: "JVBERi0xLjMKJeLjz9MKMSAwIG9iajw8L1Byb2R1Y2VyKGh0bWxkb2MgMS44LjI3IENvcHlyaWdodCAxOTk3LTIwMDYgRWFzeSBTb2Z0d2FyZSBQcm9kdWN0cywgQWxsIFJpZ2h0cyBSZXNlcnZlZC4pL0NyZWF0aW9uRGF0ZShEOjIwMTUwMjA2MTUwNTEyLTAxMDApPj5lbmRvYmoKMiAwIG9iajw8L1R5cGUvRW5jb2RpbmcvRGlmZmVyZW5jZXNbIDMyL3NwYWNlL2V4Y2xhbS9xdW90ZWRibC9udW1iZXJzaWduL2RvbGxhci9wZXJjZW50L2FtcGVyc2FuZC9xdW90ZXNpbmdsZS9wYXJlbmxlZnQvcGFyZW5yaWdodC9hc3Rlcmlzay9wbHVzL2NvbW1hL2h5cGhlbi9wZXJpb2Qvc2xhc2gvemVyby9vbmUvdHdvL3RocmVlL2ZvdXIvZml2ZS9zaXgvc2V2ZW4vZWlnaHQvbmluZS9jb2xvbi9zZW1pY29sb24vbGVzcy9lcXVhbC9ncmVhdGVyL3F1ZXN0aW9uL2F0L0EvQi9DL0QvRS9GL0cvSC9JL0ovSy9ML00vTi9PL1AvUS9SL1MvVC9VL1YvVy9YL1kvWi9icmFja2V0bGVmdC9iYWNrc2xhc2gvYnJhY2tldHJpZ2h0biAKMDAwMDEyMjYwMyAwMDAwMCBuIAowMDAwMTIyNjU1IDAwMDAwIG4gCjAwMDAxMjI3MzYgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDI3L1Jvb3QgMjYgMCBSL0luZm8gMSAwIFIvSURbPDkzNjgxOThmZWM3ODA1ZjgxYmM4ZDgzYTI3MTg3NmQ2Pjw5MzY4MTk4ZmVjNzgwNWY4MWJjOGQ4M2EyNzE4NzZkNj5dPj4Kc3RhcnR4cmVmCjEyMjkxNwolJUVPRgo=" -------------------------------------------------------------------------------- /tests/resources/responses/request1DRanges.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T09:24:49' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '916422861' 6 | 7 | response: 8 | ranges: 9 | - 10 | service_occurence: "1" 11 | service_offering: "MP5" 12 | signature: false 13 | service_type: "I" 14 | start: "RQ285500433GB" 15 | end: "RQ285510427GB" -------------------------------------------------------------------------------- /tests/resources/responses/request2DItemIDRange.yml: -------------------------------------------------------------------------------- 1 | security: 2 | timestamp: '2015-02-09T10:45:33' 3 | version: 2 4 | application_id: '111111113' 5 | transaction_id: '350301134' 6 | 7 | response: 8 | start: '0002250001' 9 | end: '0002500000' -------------------------------------------------------------------------------- /tests/unit/Connector/soapConnectorTest.php: -------------------------------------------------------------------------------- 1 | given($this->newTestedInstance->setEndpoint(ENDPOINT)) 15 | ->string($this->testedInstance->getEndpoint()) 16 | ->contains('ShippingAPI_V2_0_9.wsdl'); 17 | } 18 | 19 | 20 | function testSchemaWSDLCompatible() { 21 | $requests = ['cancelShipment', 'createManifest']; 22 | 23 | foreach ($requests as $req) { 24 | $action = $this->getTestRequest($req, $with_response = TRUE); 25 | 26 | $this->array($action['request'])->isEqualTo($action['response']); 27 | 28 | $this 29 | ->given($this->newTestedInstance->setSoapClient($this->getMockSoapClient())) 30 | ->object($response = $this->testedInstance->doRequest($req, $action['request'])) 31 | ->string($response->integrationHeader->version) 32 | ->isEqualTo("2"); 33 | } 34 | } 35 | 36 | 37 | function testWSSecurity() { 38 | $this 39 | ->given($this->newTestedInstance->setSoapClient($this->getMockSoapClient())) 40 | ->object($this->testedInstance->doRequest('cancelShipment', $this->getTestRequest('cancelShipment'))); 41 | 42 | $this 43 | ->string($this->testedInstance->getAPIFormattedRequest()) 44 | ->contains('Username>blah<') 45 | ->contains('Created>' . date_create()->format('Y-m-d')) // Do not run this test at midnight! (Or get it wet). 46 | ->matches('/:Password>.+=string(self::doCleanGBPhone('+44 1234 567 890', ['stripCountryCode' => TRUE])) 16 | ->isEqualTo('01234 567 890'); 17 | 18 | $this 19 | ->string(self::doCleanGBPhone('0044 1234 567 890', ['stripCountryCode' => TRUE])) 20 | ->isEqualTo('01234 567 890'); 21 | 22 | $this 23 | ->string(self::doCleanGBPhone('+44 (0)1234 567 890', ['stripCountryCode' => TRUE, 'stripBlanks' => TRUE])) 24 | ->isEqualTo('01234567890'); 25 | } 26 | 27 | 28 | function testGBPostcodeFormatter() { 29 | $this->string(self::doFormatGBPostcode('EH10 4BF'))->isEqualTo('EH10 4BF'); // Correct 30 | $this->string(self::doFormatGBPostcode('Eh10 4bF'))->isEqualTo('EH10 4BF'); // Wrong Case 31 | $this->string(self::doFormatGBPostcode('Eh104bF'))->isEqualTo('EH10 4BF'); // No Space 32 | $this->string(self::doFormatGBPostcode('Eh104 bF'))->isEqualTo('EH10 4BF'); // Wrong Space 33 | 34 | $this 35 | ->string(self::doFormatGBPostcode('Eh104 bF', ['check_country' => 'input:country'], ['input' => ['country' => 'GB']])) 36 | ->isEqualTo('EH10 4BF'); // Wrong Space with country check == GB 37 | 38 | $this 39 | ->string(self::doFormatGBPostcode('123456', ['check_country' => 'input:country'], ['input' => ['country' => 'FR']])) 40 | ->isEqualTo('123456'); // Non GB Postcode 41 | } 42 | 43 | 44 | 45 | function testSkipIfEmpty() { 46 | $schema = ['_pre_filter' => ['SkipThisIfThatEmpty' => 'input:bar']]; 47 | 48 | $helper = ['input' => ['bar' => 'bq', 'baz' => 'inga']]; 49 | 50 | $this->string(self::filter('foo', $schema, 'pre', $helper))->isEqualTo('foo'); 51 | 52 | $helper['input']['bar'] = ''; 53 | 54 | $this 55 | ->exception(function () use ($schema, $helper) { self::filter('foo', $schema, 'pre', $helper); }) 56 | ->isInstanceOf('\RoyalMail\Exception\StructureSkipFieldException'); 57 | } 58 | 59 | 60 | function testRounding() { 61 | $this->integer(self::doRound(100, []))->isEqualTo(100); 62 | $this->integer(self::doRound(123.45, []))->isEqualTo(123); 63 | $this->integer(self::doRound(123.53, []))->isEqualTo(124); 64 | } 65 | } -------------------------------------------------------------------------------- /tests/unit/Helper/DataTest.php: -------------------------------------------------------------------------------- 1 | ['key' => 'T', 'value' => 'Tracked Returns', 'size' => 7], 13 | 'bfpo_formats' => ['key' => 'FAE', 'value' => 'SD 101 - 500G £2500 COMP', 'size' => 13], 14 | 'country_codes' => ['key' => 'ML', 'value' => 'CHRISTMAS ISLANDS (PACIFIC), KIRIBATI', 'size' => 260], 15 | 'service_enhancements' => ['key' => '11', 'value' => 'SMS & E-Mail Notification', 'size' => 14], 16 | 'service_formats' => ['key' => 'F', 'value' => 'International Format Not Applicable', 'size' => 8], 17 | 'service_offerings' => ['key' => 'TSS', 'value' => 'INTL BUS MAIL L LTR ZERO SRT ECONOMY MCH', 'size' => 20], 18 | ]; 19 | 20 | 21 | function testDataLoading() { 22 | foreach ($this->data_checks as $store => $check) { 23 | $this 24 | ->given($this->newTestedInstance) 25 | ->array($this->testedInstance[$store]) 26 | ->hasSize($check['size']) 27 | ->hasKey($check['key']) 28 | ->contains($check['value']); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/unit/Helper/StructureTest.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtledesign/royalmail-php/750c4277bcff5466ac73927ece8aae439b74efde/tests/unit/Helper/StructureTest.php -------------------------------------------------------------------------------- /tests/unit/Request/BuilderTest.php: -------------------------------------------------------------------------------- 1 | string(ReqBuilder::processSingleProperty(['_default' => 'foo'], @$not_defined))->isEqualTo('foo'); 17 | $this->string(ReqBuilder::processSingleProperty(['_default' => 'bar'], '0'))->isEqualTo('bar'); // Beware. 18 | $this->string(ReqBuilder::processSingleProperty(['_default' => '0044'], NULL))->isEqualTo('0044'); 19 | } 20 | 21 | 22 | function testPathCreation() { 23 | $this 24 | ->array(ReqBuilder::addProperty(['us' => 'chickens'], ['_key' => 'foo/bar/baz'], 0, 'kaboom!')) 25 | ->isEqualTo(['us' => 'chickens', 'foo' => ['bar' => ['baz' => 'kaboom!']]]); 26 | 27 | $this 28 | ->array(ReqBuilder::addProperty([], ['_key' => 'foo/bar/baz'], 0, 'kaboom!')) 29 | ->isEqualTo(['foo' => ['bar' => ['baz' => 'kaboom!']]]); 30 | 31 | 32 | $this 33 | ->array(ReqBuilder::addProperty(['us' => 'chickens'], [], 'fizz', 'buzz')) 34 | ->isEqualTo(['us' => 'chickens', 'fizz' => 'buzz']); 35 | } 36 | 37 | 38 | function testValidRequests() { 39 | $helper = new \RoyalMail\Helper\Data(['override_defaults' => ['_disable_includes' => TRUE]]); 40 | $requests = glob(RESOURCES_DIR . '/requests/*.yml'); 41 | 42 | foreach ($requests as $req_file) { 43 | $req_name = basename($req_file, '.yml'); 44 | $setup = $this->getTestSchema('requests/' . $req_name); 45 | 46 | $valid = $setup['valid']; 47 | 48 | $this 49 | ->array(ReqBuilder::build(preg_replace('/_\w+$/', '', $req_name), $valid['request'], $helper)) 50 | ->isEqualTo($valid['expect']); 51 | } 52 | } 53 | 54 | 55 | function testMultiplePropertyCreation() { 56 | $tests = self::getTestConfigs('misc_builder_tests'); 57 | 58 | $musi = $tests['multiple_property_single_element']; 59 | $this 60 | ->array(ReqBuilder::processSchema($musi['schema'], $musi['values'])) 61 | ->isEqualTo($musi['expect']); 62 | 63 | } 64 | 65 | 66 | function testCreateShipmentWeight() { 67 | $tests = self::getTestConfigs('misc_builder_tests'); 68 | 69 | $weight = $tests['weight_from_create_shipment']; 70 | 71 | $this 72 | ->array(ReqBuilder::processSchema($weight['schema'], $weight['values'])) 73 | ->isEqualTo($weight['expect']); 74 | } 75 | 76 | 77 | 78 | static function getTestConfigs($key) { 79 | return Yaml::parse(file_get_contents(RESOURCES_DIR . '/' . $key . '.yml')); 80 | } 81 | } -------------------------------------------------------------------------------- /tests/unit/Response/InterpreterTest.php: -------------------------------------------------------------------------------- 1 | getMockResponse(); 17 | 18 | $this->string(Inter::extractValue($mock_response, ['_extract' => 'first']))->isEqualTo('Who'); 19 | $this->array(Inter::extractValue($mock_response, ['_extract' => 'first', '_multiple' => TRUE]))->hasSize(1)->contains('Who'); 20 | $this->array(Inter::extractValue($mock_response, ['_extract' => 'field', '_multiple' => TRUE]))->hasSize(2); 21 | 22 | $this->array(Inter::addProperty( 23 | [], 24 | ['where' => ['_extract' => 'position'], 'who' => ['_extract' => 'player']], 'field', 25 | NULL, 26 | [], 27 | ['source' => $mock_response->field[0]] 28 | ))->isEqualTo(['field' => ['where' => 'left', 'who' => 'Why']]); 29 | } 30 | 31 | 32 | function testIntepretation() { 33 | $schema = [ 34 | 'properties' => ['First' => ['_extract' => 'first']], 35 | ]; 36 | 37 | $this->array(Inter::processSchema($schema, $this->getMockResponse()))->isEqualTo(['First' => 'Who']); 38 | 39 | $mock_response = $this->getMockResponse(); 40 | 41 | $this->array(Inter::processSchema( 42 | ['properties' => ['where' => ['_extract' => 'position'], 'who' => ['_extract' => 'player']]], 43 | $mock_response->field[0] 44 | ))->isEqualTo(['where' => 'left', 'who' => 'Why']); 45 | 46 | 47 | $nested_schema = [ 48 | 'properties' => [ 49 | 'field' => [ 50 | '_extract' => 'field', 51 | '_multiple' => TRUE, 52 | 'where' => ['_extract' => 'position'], 53 | 'who' => ['_extract' => 'player'], 54 | ] 55 | ] 56 | ]; 57 | 58 | $this 59 | ->array(Inter::processSchema($nested_schema, $mock_response)) 60 | ->isEqualTo([ 61 | 'field' => [ 62 | ['where' => 'left', 'who' => 'Why'], 63 | ['where' => 'center', 'who' => 'Because'], 64 | ] 65 | ]); 66 | } 67 | 68 | 69 | function testResponseConversions() { 70 | $requests = glob(MODULE_ROOT . 'src/Response/schema/*.yml'); 71 | 72 | foreach ($requests as $req) { 73 | $req = basename($req, '.yml'); 74 | 75 | if (preg_match('/^integration/', $req)) continue; 76 | 77 | $expect = $this->getTestSchema('responses/' . $req); 78 | $test = $this->getTestRequest($req, $with_response = TRUE); 79 | 80 | $response = (new Soap()) 81 | ->setSoapClient($this->getMockSoapClient()) 82 | ->doRequest($req, $test['request']); 83 | 84 | $this->string($response->integrationHeader->version)->isEqualTo("2"); 85 | 86 | $this 87 | ->given($this->newTestedInstance) 88 | ->object($response = $this->testedInstance->loadResponse($req, $response, ['params' => ['text_only' => TRUE]])); 89 | 90 | $this->array($response->getSecurityInfo())->isEqualTo($expect['security']); 91 | $this->array($response->getResponseEncoded())->isEqualTo($expect['response']); 92 | 93 | $this->boolean($response->hasErrors())->isFalse(); 94 | $this->boolean($response->hasWarnings())->isEqualTo(@$expect['has_warnings'] ?: FALSE); 95 | } 96 | } 97 | 98 | 99 | function testBinaries() { 100 | $requests = ['printLabel']; 101 | 102 | if (! file_exists(TMP_DIR)) mkdir(TMP_DIR); 103 | 104 | foreach ($requests as $req) { 105 | $test = $this->getTestRequest($req, $with_response = TRUE); 106 | 107 | $response = (new Soap()) 108 | ->setSoapClient($this->getMockSoapClient()) 109 | ->doRequest($req, $test['request']); 110 | 111 | $this 112 | ->given($this->newTestedInstance) 113 | ->object($response = $this->testedInstance->loadResponse($req, $response)); 114 | 115 | $this->boolean($response->hasBinaries())->isTrue(); 116 | 117 | if (! function_exists('finfo_open')) return; 118 | 119 | $finfangfo = finfo_open(FILEINFO_MIME_TYPE); 120 | 121 | foreach ($response->getBinariesInfo() as $key => $mime) { 122 | if (empty($response[$key])) continue; 123 | 124 | $test_file = TMP_DIR . '/' . $key . '.test'; 125 | 126 | file_put_contents($test_file, $response[$key]); 127 | 128 | $this->string(finfo_file($finfangfo, $test_file))->isEqualTo($mime); 129 | 130 | unlink($test_file); 131 | } 132 | } 133 | } 134 | 135 | 136 | function testErrorResponses() { 137 | $errors = [ 138 | 'Single' => [[ 139 | 'code' => 'E1084', 140 | 'message' => 'shipmentType is a required field' 141 | ]], 142 | ]; 143 | 144 | $errors['Multiple'] = array_merge($errors['Single'], [[ 145 | 'code' => 'E1085', 146 | 'message' => 'another error' 147 | ]]); 148 | 149 | 150 | foreach (array_keys($errors) as $layout) { 151 | $soap = (new Soap()) 152 | ->setSoapClient($this->getMockSoapClient()->setPostfix($layout . 'ErrorResponse.xml')) 153 | ->doRequest('cancelShipment', $this->getTestRequest('cancelShipment')); 154 | 155 | $this 156 | ->given($this->newTestedInstance) 157 | ->object($response = $this->testedInstance->loadResponse('cancelShipment', $soap, ['params' => ['text_only' => TRUE]])); 158 | 159 | $this->boolean($response->hasErrors())->isTrue(); 160 | $this->boolean($response->hasWarnings())->isFalse(); 161 | $this->boolean($response->succeeded())->isFalse(); 162 | 163 | $this->array($response->getErrors())->isEqualTo($errors[$layout]); 164 | } 165 | } 166 | 167 | 168 | function testWarningResponses() { 169 | $warnings = [ 170 | 'Single' => [[ 171 | 'code' => 'W1084', 172 | 'message' => 'shipmentType would be nice to have...' 173 | ]], 174 | ]; 175 | 176 | $warnings['Multiple'] = array_merge($warnings['Single'], [[ 177 | 'code' => 'W1085', 178 | 'message' => 'another warning' 179 | ]]); 180 | 181 | 182 | foreach (array_keys($warnings) as $layout) { 183 | $soap = (new Soap()) 184 | ->setSoapClient($this->getMockSoapClient()->setPostfix($layout . 'WarningResponse.xml')) 185 | ->doRequest('cancelShipment', $this->getTestRequest('cancelShipment')); 186 | 187 | $this 188 | ->given($this->newTestedInstance) 189 | ->object($response = $this->testedInstance->loadResponse('cancelShipment', $soap, ['params' => ['text_only' => TRUE]])); 190 | 191 | $this->boolean($response->hasErrors())->isFalse(); 192 | $this->boolean($response->hasWarnings())->isTrue(); 193 | $this->boolean($response->succeeded())->isTrue(); 194 | 195 | $this->array($response->getWarnings())->isEqualTo($warnings[$layout]); 196 | } 197 | } 198 | 199 | 200 | function testWarningAndErrorResponse() { 201 | $soap = (new Soap()) 202 | ->setSoapClient($this->getMockSoapClient()->setPostfix('ErrorAndWarningResponse.xml')) 203 | ->doRequest('cancelShipment', $this->getTestRequest('cancelShipment')); 204 | 205 | $this 206 | ->given($this->newTestedInstance) 207 | ->object($response = $this->testedInstance->loadResponse('cancelShipment', $soap, ['params' => ['text_only' => TRUE]])); 208 | 209 | $this->boolean($response->hasErrors())->isTrue(); 210 | $this->boolean($response->hasWarnings())->isTrue(); 211 | $this->boolean($response->succeeded())->isFalse(); 212 | 213 | $this->array($response->getWarnings())->isEqualTo([[ 214 | 'code' => 'W1084', 215 | 'message' => 'shipmentType would be nice to have...' 216 | ]]); 217 | 218 | $this->array($response->getErrors())->isEqualTo([[ 219 | 'code' => 'E1084', 220 | 'message' => 'shipmentType is a required field' 221 | ]]); 222 | } 223 | 224 | 225 | function testPostFilters() { 226 | $dates_schema = [ 227 | 'properties' => [ 228 | 'today' => ['_extract' => 'roles/catcher', '_post_filter' => 'ObjectifyDate'], 229 | 'tomorrow' => ['_extract' => 'roles/catcher', '_post_filter' => 'ObjectifyDate'], 230 | ] 231 | ]; 232 | 233 | $this->array($response = Inter::processSchema($dates_schema, $this->getMockResponse()))->hasSize(2); 234 | 235 | $this->object($response['today'])->string($response['today']->format('d'))->isEqualTo(date_create()->format('d')); 236 | } 237 | 238 | 239 | 240 | function getMockResponse() { 241 | $field_left = new Obj; 242 | $field_left->position = 'left'; 243 | $field_left->player = 'Why'; 244 | 245 | $field_center = new Obj; 246 | $field_center->position = 'center'; 247 | $field_center->player = 'Because'; 248 | 249 | $response = new Obj; 250 | $response->first = 'Who'; 251 | $response->second = 'What'; 252 | $response->third = 'I Don\'t know'; 253 | 254 | $response->field = [$field_left, $field_center]; 255 | 256 | $response->roles = new Obj; 257 | $response->roles->pitcher = date_create()->add(new \DateInterval('P1D'))->format('Y-m-d'); 258 | $response->roles->catcher = date_create()->format('Y-m-d'); 259 | $response->roles->shortstop = 'I Don\'t Give a Darn'; 260 | 261 | return $response; 262 | } 263 | } -------------------------------------------------------------------------------- /tests/unit/RoyalMailTest.php: -------------------------------------------------------------------------------- 1 | given($this->newTestedInstance) 14 | ->object($this->testedInstance->getDataHelper()) 15 | ->isInstanceOf('\RoyalMail\Helper\Data'); 16 | } 17 | 18 | 19 | function testConnectorFactory() { 20 | $this 21 | ->given($this->newTestedInstance) 22 | ->object($connector = $this->testedInstance->getConnector()) 23 | ->isInstanceOf('\RoyalMail\Connector\soapConnector') 24 | ->object($connector->getSoapClient()) 25 | ->isInstanceOf('\RoyalMail\Connector\MockSoapClient'); 26 | } 27 | 28 | 29 | function testSoapFaultHandling() { 30 | // Remote (would only work with live details) 31 | $interface = new \RoyalMail\RoyalMail([ 32 | 'mode' => 'onboarding', 33 | 'application_id' => '9876543210', 34 | 'transaction_id' => 'order-234', 35 | 'username' => 'my-username', 36 | 'password' => 'my-password', 37 | 'endpoint' => NULL, 38 | 'soap_client_options' => [ 39 | 'uri' => MODULE_ROOT . 'reference/ShippingAPI_V2_0_9.wsdl', 40 | 'location' => 'https://api.royalmail.com/shipping/onboarding', 41 | 'exception' => FALSE, 42 | ], 43 | ]); 44 | 45 | $this 46 | ->object($connector = $interface->getConnector()) 47 | ->isInstanceOf('\RoyalMail\Connector\soapConnector') 48 | ->object($connector->getSoapClient()) 49 | ->isInstanceOf('\RoyalMail\Connector\TDSoapClient') 50 | ->object($response = $interface->cancelShipment($this->getRequestParams())) 51 | ->isInstanceOf('\RoyalMail\Response\Interpreter') 52 | ->boolean($response->succeeded()) 53 | ->isFalse() 54 | ->object($debug = $response->getDebugInfo()) 55 | ->isInstanceOf('\RoyalMail\Exception\ResponseException') 56 | ->string($debug->getSentRequest()) 57 | ->matches('/SOAP-ENV:Envelope/'); 58 | } 59 | 60 | 61 | function testBuildRequest() { 62 | $this 63 | ->given($this->newTestedInstance) 64 | ->array($this->testedInstance->buildRequest('cancelShipment', $this->getRequestParams())) 65 | ->hasKeys(['cancelShipments', 'integrationHeader']) 66 | ->hasSize(2); 67 | } 68 | 69 | 70 | function testRequestSending() { 71 | $this 72 | ->given($this->newTestedInstance) 73 | ->array($built = $this->testedInstance->buildRequest('cancelShipment', $this->getRequestParams())) 74 | ->object($response = $this->testedInstance->send('cancelShipment', $built)) 75 | ->isInstanceOf('\stdClass') 76 | ->string($response->completedCancelInfo->status->status->statusCode->code) 77 | ->isEqualTo('Cancelled'); 78 | } 79 | 80 | 81 | function testResponseIntepretation() { 82 | $this 83 | ->given($this->newTestedInstance) 84 | ->array($built = $this->testedInstance->buildRequest('cancelShipment', $this->getRequestParams())) 85 | ->object($response = $this->testedInstance->send('cancelShipment', $built)) 86 | ->object($intepreted = $this->testedInstance->interpretResponse('cancelShipment', $response)) 87 | ->isInstanceOf('\RoyalMail\Response\Interpreter') 88 | ->boolean($intepreted->succeeded()) 89 | ->isTrue() 90 | ->string($intepreted['status']) 91 | ->isEqualTo('Cancelled') 92 | ->array($intepreted->asArray()) // https://github.com/turtledesign/royalmail-php/issues/12 comment #4 93 | ->hasKeys(['status', 'updated', 'cancelled_shipments']); 94 | } 95 | 96 | 97 | function testEndtoEndAPICall() { 98 | $this 99 | ->given($this->newTestedInstance) 100 | ->object($intepreted = $this->testedInstance->processAction('cancelShipment', $this->getRequestParams())) 101 | ->isInstanceOf('\RoyalMail\Response\Interpreter') 102 | ->boolean($intepreted->succeeded()) 103 | ->isTrue(); 104 | } 105 | 106 | 107 | function testSettingAuthDetails() { 108 | $rm = new \RoyalMail\RoyalMail([ 109 | 'application_id' => '9876543210', 110 | 'transaction_id' => 'order-234', 111 | 'username' => 'my-username', 112 | 'password' => 'my-password', 113 | 'soap_client_options' => ['local_cert' => __FILE__ ], // Doesn't do anything, used to check the parameter makes it through. 114 | ]); 115 | 116 | 117 | $this 118 | ->object($rm->cancelShipment(array_diff_key($this->getRequestParams(), ['integrationHeader' => 1]))) 119 | ->string($req_xml = $rm->getConnector()->getAPIFormattedRequest()); 120 | } 121 | 122 | 123 | 124 | function testGetAvailableActions() { 125 | $this 126 | ->given($this->newTestedInstance) 127 | ->array($this->testedInstance->getAvailableActions()) 128 | ->isNotEmpty(); 129 | } 130 | 131 | 132 | function testMagicRequests() { 133 | $this 134 | ->given($this->newTestedInstance) 135 | ->object($intepreted = $this->testedInstance->cancelShipment($this->getRequestParams())) 136 | ->isInstanceOf('\RoyalMail\Response\Interpreter') 137 | ->boolean($intepreted->succeeded()) 138 | ->isTrue(); 139 | } 140 | 141 | 142 | function getRequestParams() { 143 | return $this->getSampleRequest('cancelShipment'); 144 | } 145 | } -------------------------------------------------------------------------------- /tests/unit/Validator/ValidatesTest.php: -------------------------------------------------------------------------------- 1 | TRUE, 14 | '_validate' => [ 15 | ['Length' => ['max' => 100]], 16 | ['Email' => []] 17 | ] 18 | ]; 19 | 20 | $this 21 | ->string(self::validate($schema, 'test@example.com', NULL)) 22 | ->isEqualTo('test@example.com'); 23 | 24 | $this 25 | ->exception(function () use ($schema) { self::validate($schema, 'not.valid.email', NULL); }) 26 | ->message 27 | ->contains('not a valid email'); 28 | 29 | // Auto added checkNotBlank due to _required on schema. 30 | $this 31 | ->exception(function () use ($schema) { self::validate($schema, '', NULL); }) 32 | ->message 33 | ->contains('can not be blank'); 34 | } 35 | 36 | 37 | function testNotBlank() { 38 | $this 39 | ->exception(function () { self::constrain('', 'NotBlank', NULL); }) 40 | ->message 41 | ->contains('can not be blank'); 42 | 43 | $this 44 | ->exception(function () { self::constrain(NULL, 'NotBlank', NULL); }) 45 | ->message 46 | ->contains('can not be blank'); 47 | 48 | $this->string(self::constrain('not a blank value', 'NotBlank'))->isEqualTo('not a blank value'); 49 | } 50 | 51 | 52 | function testDate() { 53 | $date_constraint = [ 54 | 'format' => 'Y-m-d', 55 | 'min' => '+0 days', 56 | 'max' => '+30 days' 57 | ]; 58 | 59 | // Too Soon! 60 | $this 61 | ->exception(function () use ($date_constraint) { self::constrain(date_create()->sub(new \DateInterval('P1D'))->format('Y-m-d'), 'Date', $date_constraint); }) 62 | ->message 63 | ->contains('earlier than'); 64 | 65 | 66 | // Too Late! 67 | $this 68 | ->exception(function () use ($date_constraint) { self::constrain(date_create()->add(new \DateInterval('P32D'))->format('Y-m-d'), 'Date', $date_constraint); }) 69 | ->message 70 | ->contains('later than'); 71 | 72 | 73 | // Too Late! (with DateTime object). 74 | $this 75 | ->exception(function () use ($date_constraint) { self::constrain(date_create()->add(new \DateInterval('P32D')), 'Date', $date_constraint); }) 76 | ->message 77 | ->contains('later than'); 78 | 79 | $ds = date_create()->format('Y-m-d'); 80 | $this 81 | ->string(self::constrain($ds, 'Date', $date_constraint)) 82 | ->isEqualTo($ds); 83 | 84 | $ds = date_create()->add(new \DateInterval('P29D'))->format('Y-m-d'); 85 | $this 86 | ->string(self::constrain($ds, 'Date', $date_constraint)) 87 | ->isEqualTo($ds); 88 | } 89 | 90 | 91 | function testEmail() { 92 | $this 93 | ->string(self::constrain('test@example.com', 'Email', [])) 94 | ->isEqualTo('test@example.com'); 95 | 96 | $this 97 | ->exception(function () { self::constrain('not.valid.email', 'Email', []); }) 98 | ->message 99 | ->contains('not a valid email'); 100 | } 101 | 102 | 103 | function testCustomValidationException() { 104 | $this->exception(function () { self::constrain(NULL, 'NotBlank'); })->hasMessage('can not be blank'); // Default NotBlank message NULL != scalar. 105 | $this->exception(function () { self::constrain('', 'NotBlank'); })->hasMessage('can not be blank []'); 106 | $this->exception(function () { self::constrain('', 'NotBlank', ['message' => 'foo']); })->hasMessage('foo []'); 107 | } 108 | 109 | 110 | function testHasValue() { 111 | $this->boolean(self::is(['foo' => 'bar'], ['required' => 'foo']))->isTrue(); 112 | $this->boolean(self::is(['foo' => ['bar' => 'baz']], ['required' => 'foo.bar']))->isTrue(); 113 | } 114 | 115 | 116 | function testThisRequiredWhenThat() { 117 | $this // Required, and set (array). 118 | ->string(self::constrain('foo', 'ThisRequiredWhenThat', ['that' => 'input:bar', 'is' => ['baz', 'kaboom']], ['input' => ['bar' => 'kaboom']])) 119 | ->isEqualTo('foo'); 120 | 121 | $this // Required, and set (string). 122 | ->string(self::constrain('foo', 'ThisRequiredWhenThat', ['that' => 'input:bar', 'is' => 'kaboom'], ['input' => ['bar' => 'kaboom']])) 123 | ->isEqualTo('foo'); 124 | 125 | $this // Required, and empty. 126 | ->exception(function () { 127 | self::constrain('', 'ThisRequiredWhenThat', ['that' => 'input:bar', 'is' => ['baz', 'kaboom']], ['input' => ['bar' => 'kaboom']]); 128 | })->message->contains('required when'); 129 | 130 | $this // Not Required, and not set. 131 | ->string(self::constrain('', 'ThisRequiredWhenThat', ['that' => 'input:bar', 'is' => ['baz', 'kaboom']], ['input' => ['bar' => 'boing']])) 132 | ->isEqualTo(''); 133 | 134 | $this // Not Required, and set (currently no option to skip if not required). 135 | ->string(self::constrain('foo', 'ThisRequiredWhenThat', ['that' => 'input:bar', 'is' => ['baz', 'kaboom']], ['input' => ['bar' => 'boing']])) 136 | ->isEqualTo('foo'); 137 | } 138 | 139 | 140 | function testGBPostcode() { 141 | $this // Required and valid. 142 | ->string(self::constrain('EH10 4BF', 'GBPostcode', ['check_country' => 'input:country'], ['input' => ['country' => 'GB']])) 143 | ->isEqualTo('EH10 4BF'); 144 | 145 | $this // Required and valid - no country check. 146 | ->string(self::constrain('EH10 4BF', 'GBPostcode', [], [])) 147 | ->isEqualTo('EH10 4BF'); 148 | 149 | $this // Overseas (and would be invalid). 150 | ->string(self::constrain('123456789', 'GBPostcode', ['check_country' => 'input:country'], ['input' => ['country' => 'FR']])) 151 | ->isEqualTo('123456789'); 152 | 153 | $this // Invalid. 154 | ->exception(function () { self::constrain('123456789', 'GBPostcode', ['check_country' => 'input:country'], ['input' => ['country' => 'GB']]); }) 155 | ->message->contains('not a valid UK postcode'); 156 | 157 | $this // Invalid (more subtle). 158 | ->exception(function () { self::constrain('EH101 4BF', 'GBPostcode', ['check_country' => 'input:country'], ['input' => ['country' => 'GB']]); }) 159 | ->message->contains('not a valid UK postcode'); 160 | } 161 | 162 | 163 | function testRange() { 164 | $this->integer(self::constrain(100, 'Range', ['min' => 10, 'max' => 9999]))->isEqualTo(100); 165 | 166 | $this 167 | ->exception(function () { self::constrain(1, 'Range', ['min' => 10, 'max' => 9999]); }) 168 | ->message->contains('value should be over'); 169 | 170 | $this 171 | ->exception(function () { self::constrain(10000, 'Range', ['min' => 10, 'max' => 9999]); }) 172 | ->message->contains('value should be under'); 173 | 174 | } 175 | } --------------------------------------------------------------------------------