PHP API Framework
A lightweight PHP API Framework
Great for CRUD applications, (*1)
, (*2)
@PSR2 and @Symfony Compliant, (*3)
The best way to install Spry and use it is through the CLI. https://github.com/ggedde/spry-cli, (*4)
composer global require ggedde/spry-cli
Please reference the Installation Process on the CLI Page.
Then from the command line:, (*5)
spry new [project_name] cd [project_name]
An enpoint is automatically created for you, (*6)
public/index.php, (*7)
include dirname(__DIR__).'/vendor/autoload.php'; include dirname(__DIR__).'/spry/init.php';
, (*8)
composer require ggedde/spry
You will need to add the composer autoloader if it has not already been added.
Then use the run
method and include a path to your config file or include a config object.
See configuration, (*9)
Example:, (*10)
include_once '/vendor/autoload.php'; Spry\Spry::run('../config.php');
spry new [project_name] cd [project_name]
- public/ index.php - spry/ - components/ - logs/ - api.log - php.log - config.php - init.php
spry up
spry test
spry component MyComponent
Now View and Edit spry/components/MyComponent.php
, (*11)
spry migrate
spry test
Thats It!
Happy Coding, (*12)
Well, I guess you might need more info. Most of all your coding will be in the components files you create and the rest will most likely be in the configuration file. See more details below :), (*13)
, (*14)
Spry requires a config file or a config object., (*15)
When using a config file Spry will pass a pre-initialized $config object to the file. You will just need to set the variables within the object, (*16)
Example Config File:, (*17)
<?php $config->salt = ''; $config->endpoint = 'http://localhost:8000'; $config->componentsDir = __DIR__.'/components'; ...
Setting | Type | Default | Description |
---|---|---|---|
componentsDir | String | __DIR__.'/components' | Directory where Components are stored. Component Filenames must match the Class name of the Component. |
db | Array | [] | Database Object See Database documentation |
dbProvider | String | 'Spry\\SpryProvider\\SpryDB' | Database Provider Class See Database documentation |
endpoint | String | 'http://localhost:8000' | Spry Server Endpoint url. Used for internal requests only. |
logger | Array | [] | Logger Object See Logger documentation |
loggerProvider | Array | 'Spry\\SpryProvider\\SpryLogger' | Logger Provider Class See Logger documentation |
projectPath | String | __DIR__ | Path to Spry Project. Default is to use the same directory the config.php file is in. |
responseCodes | Array | [] | Array of Response Codes See Response Codes documentation |
responseHeaders | Array | [] | Default Response Codes |
routes | Array | [] | Array of Routes See Routes documentation |
salt | String | '' | Salt for Security. Change this to be Unique for each one of your API's. You should use a long and Strong key. DO NOT CHANGE IT ONCE YOU HAVE CREATED DATA. Otherwise things like logins may no longer work. |
tests | Array | [] | Array of Tests See Tests documentation |
You can access any setting by calling the config() object from Spry, (*18)
Example:, (*19)
echo Spry::config()->salt; echo Spry::config()->db['database_name']
You can add your own settings and then access them later in your component, provider or plugin, (*20)
config.php, (*21)
$config->mySetting = '123';
MyComponent.php, (*22)
echo Spry::config()->mySetting;
Spry is mainly used through components added in the componentsDir
set in the Spry config., (*23)
Components are classes within the SpryComponent Namespace.
Components can set their own Routes, Response Codes, DB Schema and Tests by using built in methods. Or you can configure everything through the config.php, (*24)
These built in Methods are Optional:, (*25)
Method | Description |
---|---|
setup | Hook on initial Spry setup |
getCodes | Returns Array of Codes and adds them to the Config |
getRoutes | Returns Array of Routes and adds them to the Config |
getSchema | Returns Array of Table Schema and adds them to the Config |
getTests | Returns Array of Tests and adds them to the Config |
Any Methods that resolve a route is called a Controller and gets passed any Parameters from the route., (*26)
Basic Example:, (*27)
<?php namespace Spry\SpryComponent; use Spry\Spry; class MyComponent { private static $id = 2; // Component ID public static function setup() { Spry::addFilter('configure', 'MyComponent::stuff'); } public static function stuff($config) { // Do Stuff return $config; } public static function getRoutes() { return [ '/items/{id}' => [ 'label' => 'Get Item', 'controller' => 'MyComponent::get', 'access' => 'public', 'methods' => 'GET', 'params' => [ 'id' => [ 'required' => true, 'type' => 'int', ], ], ], ]; } public static function getCodes() { return [ 0 => [ 'success' => ['en' => 'Successfully Retrieved Item'], 'warning' => ['en' => 'No Item with that ID Found'], 'error' => ['en' => 'Error: Retrieving Item'], ], 1 => [ 'info' => ['en' => 'Empty Results'], 'success' => ['en' => 'Successfully Retrieved All Items'], 'error' => ['en' => 'Error: Retrieving All Items'], ], ]; } public static function getSchema() { return [ 'items' => [ 'columns' => [ 'name' => [ 'type' => 'string', ], ], ], ]; } public static function getTests() { return [ 'items_get' => [ 'label' => 'Get Example', 'route' => '/items/123', 'method' => 'GET', 'params' => [], 'expect' => [ 'status' => 'success', ], ], ]; } public static function get($params = []) { $response = Spry::db()->get('items', '*', $params) return Spry::response(self::$id, 01, $response); } }
When using single file components you can set the routes in the Components by returning the routes in the getRoutes()
method., (*28)
Example:, (*29)
public static function getRoutes() { return [ '/items/{id}' => [ 'label' => 'Get Item', 'controller' => 'MyComponent::get', 'access' => 'public', 'methods' => 'GET', 'params' => [ 'id' => [ 'required' => true, 'type' => 'int', ], ], ], ]; }
$config->routes[ '/items/{id}' => [ 'label' => 'Get Item', 'controller' => 'MyComponent::get', 'access' => 'public', 'methods' => 'GET', 'params' => [ 'id' => [ 'required' => true, 'type' => 'int', ], ], ], ... ];
Setting | Type | Default | Description |
---|---|---|---|
label | String | Path Sanitized | Name used to Label Route |
controller | String | '' | Path to Component Method. Ex. MyComponent::method or NameSpace\\Framework\\Class::someMethod
|
access | String | public | Spry has a getRoutes method which will return all public routes. You can remove the route from that method by setting the access to private
|
limits | array | [] | If you have SpryRateLimits added and activated then this will allow for adding rate limits to the Route. See Rate Limits section for more details. This can either be a single rate limit array or a multi array of limits by different keys. |
methods | String\ | Array | POST | Method allowed for route. POST|GET|DELETE|PUT |
params | Array | [] | Allowed Parameters for the Route. Any Parameters passed that do not match will get ignored. You can specify Validation for each Parameter. |
params_trim | Boolean | false | Trim all Params for surrounding whitespace. This can be overwritten per param. |
Setting | Type | Default | Description |
---|---|---|---|
type | String | string | Types: integer | number | float | array | cardNumber | date | email | url | ip | domain | string | boolean | password |
between | Array[min,max] | [] | Validates Param is between 2 values |
betweenLength | Array[min,max] | [] | Validates Param length is between 2 values |
callback | Callback | Custom Callback function to return true for valid. Can be array for array(CLASS, METHOD) or closure. |
|
endsWith | String | '' | Validates Param ends with a substr. |
filter | Callback | Closure to filter value. Closure accepts one argument of the value and must return filtered value. | |
has | String | '' | Checks if Param is array and contains value. |
hasLetters | Integer | 1 | Validates Param contains x number of letters [a-zA-Z] |
hasLowercase | Integer | 1 | Validates Param contains x number of lower case letters [a-z] |
hasNumbers | Integer | 1 | Validates Param contains x number of numbers [0-9] |
hasSymbols | Integer | 1 | Validates Param contains x number of Symbols |
hasUppercase | Integer | 1 | Validates Param contains x number of upper case letters [A-Z] |
in | Array | [] | Validates Param is within Array |
length | Interger | Length of Param must match value. Accepts Length of Array or String | |
matches | String | '' | Validates Param matches Value. Strict Type comparison is enabled. |
max | Integer | Maximum count of Array or length of String | |
maxDate | Integer \ | String | 0 | If Integer then maximum days from today or String of Date formatted |
maxLength | Integer | Maximum length of String for Parameter. Strings only. | |
meta | Boolean | false | If true then the parameter will not be passed back to the controller as a Param, but as a Meta Value instead and returned back to the Controller in the second argument. Useful for route settings like pagination page, etc |
min | Integer | Minimum count of Array or length of String | |
minDate | Integer \ | String | 0 | If Integer then minimum days from today or String of Date formatted |
minLength | Integer | Maximum length of String for Parameter. Strings only. | |
notEndsWith | String | '' | Validates Param does not end with a substr. |
notMatches | String | '' | Validates Param does not match Value. Strict Type comparison is enabled. |
notStartsWith | String | '' | Validates Param does not start with a substr. |
numbersOnly | Boolean | true | Validates Param only contains numbers. |
required | Boolean \ | Array | true | Validates Param has a Value. if Array then Only requires if params within required array have value (conditional checking). |
startsWith | String | '' | Validates Param starts with a substr. |
trim | Boolean | true | Trims Param for surrounding whitespace |
validateOnly | Boolean | true | Validates the param, but does not send the value back to the controller. Ex. good for 'confirm_password' which doesn't need to be returned to the controller, but present for 'password' to match it. |
unique | Boolean | true | If Param is Array then duplicate values within array will be removed. |
Spry's default Database Provider is SpryDB based on Medoo
See SpryDB's full documentation, (*30)
This allows you to swap out the Provider later on without having to change your project code., (*31)
Spry::db()->get('items', '*', ['id' => 123]); Spry::db()->select('items', '*', ['date[>]' => '2020-01-01']); Spry::db()->insert('items', ['name' => 'test', 'date' => '2020-01-01']); Spry::db()->update('items', ['name' => 'newtest'], ['id' => 123]); Spry::db()->delete('items', ['id' => 123]);
$config->dbProvider = 'Spry\\SpryProvider\\SpryDB'; $config->db = [... ];
Spry's default Log Provider is SpryLogger
See SpryLogger's full documentation, (*32)
This allows you to swap out the Provider later on without having to change your project code., (*33)
Spry::log()->message("My Message"); Spry::log()->warning("Warning"); Spry::log()->error("Error");
$config->loggerProvider = 'Spry\\SpryProvider\\SpryLogger'; $config->logger = [... ];
, (*34)
Spry has 2 built in functions for building the response (response
and stop
)., (*35)
Spry::response($data = null, $responseCode = 0, $responseStatus = null, $meta = null, $additionalMessages = []);
You can use this to build the response data and return it from the Controller., (*36)
$responseCode: This is the Response code id from the Component.
$responseStatus: This can be either null
| info
| success
| warning
| error
If null
then the function will auto detect the status based on the value of $data
, (*37)
Example:, (*38)
$data = ['id' => 123, 'name' => 'John']; return Spry::response($data, 0);
Spry::stop($responseCode = 0, $responseStatus = null, $data = null, $additionalMessages = [], $privateData = null);
This will immediately kill the application and return the response, (*39)
Example:, (*40)
if ($error) { Spry::stop(0); }
* See Response Codes Below to see how this works, (*41)
, (*42)
public static function getCodes() { return [ 0 => [ // Get Single 'success' => ['en' => 'Successfully Retrieved Item'], 'warning' => ['en' => 'No Item with that ID Found'], 'error' => ['en' => 'Error: Retrieving Item'], ], 1 => [ // Get Multiple 'info' => ['en' => 'No Results Found'], 'success' => ['en' => 'Successfully Retrieved Items'], 'error' => ['en' => 'Error: Retrieving Items'], ], 2 => [ // Insert 'success' => ['en' => 'Successfully Created Item'], 'error' => ['en' => 'Error: Creating Item'], ], 3 => [ // Update 'success' => ['en' => 'Successfully Updated Item'], 'error' => ['en' => 'Error: Updating Item'], ], 4 => [ // Delete 'success' => ['en' => 'Successfully Deleted Item'], 'error' => ['en' => 'Error: Deleting Item'], ], ]; }
Notice you will need to add a Group Group Number for the component codes. This is not needed in the Single File Component setup, (*43)
$config->tests[ 1 => [ 0 => [ // Get Single 'success' => ['en' => 'Successfully Retrieved Item'], 'warning' => ['en' => 'No Item with that ID Found'], 'error' => ['en' => 'Error: Retrieving Item'], ], 1 => [ // Get Multiple 'info' => ['en' => 'No Results Found'], 'success' => ['en' => 'Successfully Retrieved Items'], 'error' => ['en' => 'Error: Retrieving Items'], ], 2 => [ // Insert 'success' => ['en' => 'Successfully Created Item'], 'error' => ['en' => 'Error: Creating Item'], ], 3 => [ // Update 'success' => ['en' => 'Successfully Updated Item'], 'error' => ['en' => 'Error: Updating Item'], ], 4 => [ // Delete 'success' => ['en' => 'Successfully Deleted Item'], 'error' => ['en' => 'Error: Deleting Item'], ], ], 2 => [ 0 => [ // Get Single 'success' => ['en' => 'Successfully Retrieved Other Item'], 'warning' => ['en' => 'No Other Item with that ID Found'], 'error' => ['en' => 'Error: Retrieving Other Item'], ], 1 => [ // Get Multiple 'info' => ['en' => 'No Results Found'], 'success' => ['en' => 'Successfully Retrieved Other Items'], 'error' => ['en' => 'Error: Retrieving Other Items'], ], 2 => [ // Insert 'success' => ['en' => 'Successfully Created Other Item'], 'error' => ['en' => 'Error: Creating Other Item'], ], 3 => [ // Update 'success' => ['en' => 'Successfully Updated Other Item'], 'error' => ['en' => 'Error: Updating Other Item'], ], 4 => [ // Delete 'success' => ['en' => 'Successfully Deleted Other Item'], 'error' => ['en' => 'Error: Deleting Other Item'], ], 5 => ['redirect' = ['en' => 'Depricated: This route is depricated']], 6 => ['error' = ['en' => 'Error: Custom Error Message']], 7 => ['error' = ['en' => 'Error: Another Custom Error Message']], 8 => ['error' = ['en' => 'Error: And Another Custom Error Message']], ], ... ];
The first number is to separate the Components codes by ID
Ex., (*44)
1-200 # Success For Component A 2-200 # Success For Component B
1 => [ 'success' => [ 'en' => 'Success!', 'es' => '¡Éxito!' ] ]
The first number in the code represents the code type., (*45)
[group_id]-[1]xx - the 1 represents 'Info' or 'Empty'
[group_id]-[2]xx - the 2 represents 'Success'
[group_id]-[3]xx - the 3 represents 'Redirect' or 'Depricated'
[group_id]-[4]xx - the 4 represents 'Client Error', or 'Unkown'
[group_id]-[5]xx - the 5 represents 'Server Error', (*46)
When using Spry::response() you can pass just the last 2 digits as the code and the data parameter., (*47)
Ex., (*48)
Spry::response($data, 1);
If $data is an array but empty then the response will automatically Prepend the code with a 1 and return 1-101.
If $data has a value and is not empty then the response will automatically Prepend the code with a 2 and return 1-201.
If $data is an array but not null then the response will automatically Prepend the code with a 4 and return 1-401.
If $data is false or null then the response will automatically Prepend the code with a 5 and return 1-501., (*49)
In the response there will be a key of status
The possible values for this will only be success
, error
, or unknown
, (*50)
info
, success
, and redirect
all represent a successfully response and will return success
warning
and error
represent a failed response and will return error
, (*51)
, (*52)
Spry comes with its own pre-built testing solution. You can still use other like PHPUnit, but Spry's Tests utilize's Spry's config for rapid testing. When using single file components you can set the routes in the Components by returning the routes in the getTests()
method., (*53)
Example:, (*54)
public static function getTests() { return [ 'items_insert' => [ 'label' => 'Insert Item', 'route' => '/items/insert', 'method' => 'POST', 'params' => [ 'name' => 'TestData', ], 'expect' => [ 'status' => 'success', ], ], 'items_get' => [ 'label' => 'Get Item', 'route' => '/items/{items_insert.body.id}', 'method' => 'GET', 'params' => [], 'expect' => [ 'status' => 'success', ], ], 'items_delete' => [ 'label' => 'Delete Item', 'route' => '/items/delete/', 'method' => 'POST', 'params' => [ 'id' => '{items_insert.body.id}' ], 'expect' => [ 'status' => 'success', ], ], ]; }
$config->tests[ 'items_get' => [ 'label' => 'Get Example', 'route' => '/items/123', 'params' => [], 'expect' => [ 'code' => '1-200', ], ], 'items_get' => [ 'label' => 'Get Example', 'route' => '/items/123', 'params' => [], 'expect' => [ 'body.id[>]' => 12, ], ], ... ];
Setting | Type | Default | Description |
---|---|---|---|
label | String | '' | Name used to Label Test |
route | String | '' | Route that is enabled in Spry Routes |
method | String | GET | Route that is enabled in Spry Routes |
params | Array | [] | Params passed in Route |
expect | Array | [] | Key and Value of what is expected valid from the Response. Keys accept dot notation and [>], [>=], [<], [<=], [!=], [===], [!==] comparison checks. See above. |
All Tests, (*55)
spry test
Specific Test, (*56)
spry test items_get
More Options, (*57)
spry test --verbose --repeat 10
, (*58)
Spry's default Rates Limits is SpryRateLimits
See full documentation, (*59)
This allows you to swap out the Provider later on without having to change your project code., (*60)
$config->rateLimits = [ 'driver' => 'file', 'fileDirectory' => __DIR__.'/rate_limits', 'excludeTests' => false, 'default' => [ 'by' => 'ip', 'limit' => 10, 'within' => 1, 'hook' => 'configure', 'excludeTests' => false ] ];
$config->routes = [ '/auth/login' => [ 'label' => 'Auth Login', 'controller' => 'Auth::login', 'access' => 'public', 'methods' => 'POST', 'limits' => [ 'by' => 'ip', 'limit' => 1, 'within' => 3, 'excludeTests' => false ], 'params' => [ 'email' => [ 'required' => true, 'type' => 'string', ], 'password' => [ 'required' => true, 'type' => 'string', ], ], ], ];
See full documentation, (*61)
, (*62)
Hooks & Filters Lifecycle in order of completion:
, (*63)
Name | Type | Details |
---|---|---|
initialized |
hook | Has access to the initial config, but before any filters or components |
configureInitialized |
filter | Runs after initial config has been loaded, but before setup and components have been loaded |
setup |
hook | Runs after all component setup methods have ran |
configure |
filter | Runs after all Components and Plugins have been loaded |
configure |
hook | Runs after the configure filter and after the OPTIONS pre-flight response |
getPath |
filter | Runs after route path has been recieved |
setPath |
hook | Runs after route path has been set |
setRoutes |
hook | Runs after routes has been set |
params |
filter | Runs after the Params have been fetched |
setParams |
hook | Runs after the Params have been set |
getRoute |
hook | Runs after the Route has been set |
validateParams |
filter | Runs after Params Validation. |
response |
filter | Runs after the response has been built. |
output |
filter | Runs right before the Output is returned. |
Name | Type | Details |
---|---|---|
stop |
hook | This may run at anytime when their is an error or Spry needs to stop |
database |
hook | Called after the Database connection has been made or accessing the DatabaseProvider for the first time |
dbJoin |
filter | Called right before the query is ran. Allows to filter the JOIN statement and will include $meta on the query details |
dbColumns |
filter | Called right before the query is ran. Allows to filter the FROM statement and will include $meta on the query details |
dbWhere |
filter | Called right before the query is ran. Allows to filter the WHERE statement and will include $meta on the query details |
dbData |
filter | Called right before the query is ran. Allows to filter the data for INSERT, UPDATE, REPLACE statement and will include $meta on the query details |
dbGet |
filter | Called right before the get query is ran and includes entire statement object |
dbSelect |
filter | Called right before the select query is ran and includes entire statement object |
dbInsert |
filter | Called right before the insert query is ran and includes entire statement object |
dbUpdate |
filter | Called right before the update query is ran and includes entire statement object |
dbReplace |
filter | Called right before the replace query is ran and includes entire statement object |
dbDelete |
filter | Called right before the delete query is ran and includes entire statement object |
dbHas |
filter | Called right before the has query is ran and includes entire statement object |
dbRand |
filter | Called right before the rand query is ran and includes entire statement object |
dbCount |
filter | Called right before the count query is ran and includes entire statement object |
dbAvg |
filter | Called right before the avg query is ran and includes entire statement object |
dbMax |
filter | Called right before the max query is ran and includes entire statement object |
dbMin |
filter | Called right before the min query is ran and includes entire statement object |
dbSum |
filter | Called right before the sum query is ran and includes entire statement object |
Hooks allow you to run your own code at specific times and life cycles. This is how you can run middleware and other life cycle aware code., (*64)
Spry::addHook(string $hookName, string|callable $controller, [ mixed $extraData = null [, int $order = 0 ]] ) : void
Example:, (*65)
Spry::addHook('configure', 'Spry\\SpryComponent\\MyComponent::myMethod', ['bar' => 345], 100);
Spry::runHook(string $hookName, [ mixed $data ] ) : void
Example:, (*66)
Spry::runHook('configure',['foo' => 123] [[ mixed $data = null ], mixed $meta = null ] );
Your Controller, (*67)
class MyComponent public static function myMethod($data = null, $meta = null, $extraData = null) { $data // [foo => 123] $meta // null $extraData // [bar => 345] // Do Stuff }
Filters allow you to filter data at specific times and life cycles. A filter typically requires a return value., (*68)
Spry::addFilter(string $filterName, string|callable $controller, [ mixed $extraData = null [, int $order = 0 ]] ) : void
Example:, (*69)
Spry::addFilter('configure', 'Spry\\SpryComponent\\MyComponent::myMethod', ['bar' => 345], 100);
Spry::runFilter(string $filterName, [[ mixed $data = null ], mixed $meta = null ] ) : void
Example:, (*70)
$config = Spry::runFilter('configure', $config, ['component' => 'someComponent', 'var' => 'abc']);
Your Controller, (*71)
class MyComponent public static function myMethod($config = null, $meta = null, $extraData = null) { $config // $config $meta // ['component' => 'someComponent', 'var' => 'abc'] $extraData // [bar => 345] // Do Stuff return $config; }