{"id":25813751,"url":"https://github.com/macsidigital/laravel-api-client","last_synced_at":"2025-02-28T02:49:51.979Z","repository":{"id":39802992,"uuid":"260940050","full_name":"MacsiDigital/laravel-api-client","owner":"MacsiDigital","description":null,"archived":false,"fork":false,"pushed_at":"2023-08-24T22:16:20.000Z","size":235,"stargazers_count":20,"open_issues_count":1,"forks_count":22,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-11T01:18:13.593Z","etag":null,"topics":["hacktoberfest"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MacsiDigital.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["MacsiDigital"]}},"created_at":"2020-05-03T14:19:09.000Z","updated_at":"2024-02-04T05:34:09.000Z","dependencies_parsed_at":"2024-06-18T13:58:45.316Z","dependency_job_id":"ce18a70f-2ccc-49da-b905-d9c7894cb835","html_url":"https://github.com/MacsiDigital/laravel-api-client","commit_stats":{"total_commits":58,"total_committers":8,"mean_commits":7.25,"dds":"0.18965517241379315","last_synced_commit":"460c87aab2d412a47ab5c296165aaa48287c6e09"},"previous_names":[],"tags_count":72,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacsiDigital%2Flaravel-api-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacsiDigital%2Flaravel-api-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacsiDigital%2Flaravel-api-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacsiDigital%2Flaravel-api-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MacsiDigital","download_url":"https://codeload.github.com/MacsiDigital/laravel-api-client/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241090719,"owners_count":19908005,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest"],"created_at":"2025-02-28T02:49:48.073Z","updated_at":"2025-02-28T02:49:51.972Z","avatar_url":"https://github.com/MacsiDigital.png","language":"PHP","funding_links":["https://github.com/sponsors/MacsiDigital"],"categories":[],"sub_categories":[],"readme":"# Laravel API Client\n\n## Laravel package for Building API Client's\n\n![Header Image](https://github.com/MacsiDigital/repo-design/raw/master/laravel-api-client/header.png)\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://github.com/MacsiDigital/laravel-api-client/actions?query=workflow%3ATests\"\u003e\u003cimg src=\"https://github.com/MacsiDigital/laravel-api-client/workflows/Tests/badge.svg\" style=\"max-width:100%;\"  alt=\"tests badge\"\u003e\u003c/a\u003e\n \u003ca href=\"https://packagist.org/packages/macsidigital/laravel-api-client\"\u003e\u003cimg src=\"https://img.shields.io/packagist/v/macsidigital/laravel-api-client.svg?style=flat-square\" alt=\"version badge\"/\u003e\u003c/a\u003e\n \u003ca href=\"https://packagist.org/packages/macsidigital/laravel-api-client\"\u003e\u003cimg src=\"https://img.shields.io/packagist/dt/macsidigital/laravel-api-client.svg?style=flat-square\" alt=\"downloads badge\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nAn API Client Builder Library\n\n## Installation\n\nYou can install the package via composer:\n\n```bash\ncomposer require macsidigital/laravel-api-client\n```\n\n## Versions\n\n1.0 - Laravel 5.5 - 5.8 - Deprecated and no longer maintained.\n\n2.0 - Laravel 6.0 - Maintained, again feel free to create pull requests.  This is open source which is a 2 way street.\n\n3.0 - Laravel 7.0 - 8.0 - Maintained, again feel free to create pull requests.  This is open source which is a 2 way street.\n\n## Usage\n\nThe main aim of this library is to add a common set of traits to models to be able to create, update, retrieve and delete records when accessing APIs.  Obviously all APIs are different, so you should check documentation on how best to implement with these traits.\n\nThe basic concept is you build a client in the API library that you create which extends on the models in this library.\n\n## Entry Model\n\nThe first thing to do is to create an entry model and extend the MacsiDigital/API/Support/Entry model, this is what shapes how the API will work.  It's the model that houses all the attributes, so if you want pagination or to customise something it will likely be here.  Its an Abstract model so has to be extended in your implementation.\n\nWe recommend placing it in a Support folder within your src directory.\n\nList of Attributes\n\n```php\n\t// Where the models are\n\tprotected $modelNamespace = '';\n\n    // default query string names for page and per_page fields\n    protected $perPageField = 'page_size';\n    protected $pageField = 'page';\n\n    // Should return raw responses and not models/resultsets\n    protected $raw = false;\n\n    // Should return raw responses and not models/resultsets\n    protected $raw = false;\n\n    // Should we throw exceptions in cases where a server error occurs\n    protected $throwExceptionsIfRaw = false;\n\n    // Should results be paginated by default.\n    protected $pagination = true;\n\n    // Amount of pagination results per page by default, leave blank if should not paginate\n    // Without pagination rate limits could be hit\n    protected $defaultPaginationRecords = '20';\n\n    // Max and Min pagination records per page, will vary by API server\n    protected $maxPaginationRecords = '100';\n    protected $minPaginationRecords = '1';\n\n    // If not paginated, how many queries should we allow per search, leave '' or 0 \n    // for unlimited queries. This of course will eat up any rate limits\n    protected $maxQueries = '5';\n\n    // Most APIs should include pagination data - this is the fields we should be looking for\n    // in the response to get this information.  We can use names or dot notation,\n    // so for example 'current_page' or 'meta.current_page'\n    protected $resultsPageField = 'meta.current_page';\n    protected $resultsTotalPagesField = 'meta.last_page';\n    protected $resultsPageSizeField = 'meta.per_page';\n    protected $resultsTotalRecordsField = 'meta.total';\n\n    // What operands are allowed when filtering\n    protected $allowedOperands = ['=', '!=', '\u003c', '\u003e', '\u003c=', '\u003e=', '\u003c\u003e', 'like'];\n    protected $defaultOperand = '=';\n```\n\nMost of these are self-explanatory, there is a magic method attribute in there, if we return raw data than we receive the actual response without any error checking.  if we set $throwExceptionsIfRaw = true, then we still receive raw data, but it will check to make sure the return was successful and not an error. If we receive an error it will throw a HttpException.  You can also set this on teh fly by calling with Exceptions() on a query.\n\nIn your extended Entry model implementation you have to define a newRequest function, this is where the logic for how we 1connect will go and should return a MacsiDigital\\API\\Support\\Factory object, which is better renamed to Client in your implementation, which will resolve the API Gateway Client.\n\n```php\nnamespace MacsiDigital\\Package\\Support;\n\nuse MacsiDigital\\Package\\Facades\\Client; // This should extend the MacsiDigital\\API\\Support\\Factory\nuse MacsiDigital\\API\\Support\\Entry as ApiEntry;\n\nclass Entry extends ApiEntry\n{\n    protected $modelNamespace = '\\MacsiDigital\\Package\\\\';\n\n    // Change any attributes to match API\n \n\tpublic function newRequest()\n    {\n        return Client::baseUrl(config('api.base_url'))-\u003ewithOptions(config('api.options'));\n    }\n```\n\nIf using OAuth we would have to input our OAuth Logic, or for OAuth2 something like this would work, this is xero implementation\n\n```php\n\tpublic function newRequest()\n    {\n        $config = config('xero');\n    \t$class = $config['tokenModel'];\n    \t$token = new $class('xero');\n    \tif($token-\u003ehasExpired()){\n    \t\t$token = $token-\u003erenewToken();\n    \t}\n        return Client::baseUrl($config['baseUrl'])-\u003ewithToken($token-\u003eaccessToken())-\u003ewithHeaders(['xero-tenant-id' =\u003e $token-\u003etenantId()]);\n    }\n```\n\nAs you can see all the initial logic is required on how we need to open up a Gateway, please note that the API does not handle the OAuth2 Authorisation, for this you will need to use an OAuth2 client like macsidigital/laravel-oauth2-client or league/oauth2-client.  The macsidigital client handles the routing and saving of the data to either database or file.\n\nThere is also a function in the entry class that returns the builder class to use, if you roll your own builder class, further details below, this is where you need to set the class.\n\n```php\n\tpublic function getBuilderClass() \n    {\n        return Builder::class; // By default links to MacsiDigital/API/Support/Builder\n    }\n```\n\n## RESTful API by default\n\nWe by default use standard RESTful method calls:-\n\n- get - retrieve records\n- post - create a record\n- patch - update a record\n- put - replace a record\n- delete - delete a record\n\nWe also add a find call, as most APIs have different endpoints for get and find methods.\n\n- find - retrieve a single record\n\nHowever, some APIs have different ideas on what methods to call (we are looking at you Xero).\n\nSo you override the default create and update methods by adding these attributes or methods to your models\n\n```php\n\tprotected $createMethod = 'post';\n\n    protected $updateMethod = 'patch';\n\n\tpublic function getUpdateMethod()\n    {\n        return $this-\u003eupdateMethod;\n    }\n\n    public function getCreateMethod()\n    {\n        return $this-\u003ecreateMethod;\n    }\n```\n\nAlso, some implementations, looking at you again Xero, use update methods to create models and create methods for updating models, so you may need to override the end point functions in the resource model. We have added logic to try to automatically get around this but if you are having problems then set them manually in your model.\n\nXero uses a patch request for creating models and post request for updating models.\n\n```php\n\t//Normal\n\tpublic function getPostEndPoint() \n    {\n        return $this-\u003eendPoint;\n    }\n\n    public function getPutEndPoint() \n    {\n        return $this-\u003eendPoint.'/'.$this-\u003egetKey();\n    }\n\n    //Xero\n\tpublic function getPostEndPoint() \n    {\n        return $this-\u003eendPoint.'/'.$this-\u003egetKey();\n    }\n\n    public function getPutEndPoint() \n    {\n        return $this-\u003eendPoint;\n    }\n```\n\n## Retrieving models\n\nYou can retrieve a single model by either using the find method and passing an ID, or an array of IDs, or by running a query and returning first() or last();\n\n```php\n\t$user = API::user()-\u003efind('ID');\n\n\t$user = API::user()-\u003ewhere('Name', 'Bob')-\u003efirst(); // First occurrence\n\n    $user = API::user()-\u003ewhere('Name', 'Bob')-\u003elast(); // Last occurrence\n```\n\nYou can also use get and all to retrieve many models, this will return a Result Set, which is an enhanced Laravel Collection.\n\n```php\n\t$users = API::account()-\u003eall();\n\n\t$users = API::account()-\u003ewhere('Type', 'Admin')-\u003eget();\n```\n\nSome APIs (Yes Xero) return single results wrapped in an array, like multi record searches.  So you can override the hydrate() method on the builder model.\n\n```php\n\tprotected function hydrate($response)\n    {\n        return $this-\u003eresource-\u003enewFromBuilder($response-\u003ejson()[$this-\u003egetApiDataField()][0]);\n    }\n```\n\n## Result Sets\n\nAfter we run a multi result return type like all() and get(), we are returned a result set.  This will take care of any pagination and can be used to dip back into the Gateway to retrieve the next lot of results, houses information on what page we navigated, what is the next page etc.  This is dependant on the API, and we try to utilise the APIs returned meta where possible.\n\n### all() return method\n\nWith the all() method we try to retrieve all results by recursively hitting the API endpoint, because this can use rate limits quite quickly there is a $maxQueries attribute in our entry model, once set this is the max amount of queries that can be hit in a recursive call.\n\nIf the max is hit and you require more results then you can call the retrieveNextPage() method which will add the next round of queries to the result set.\n\n### get() return method\n\nThis will return up to the max number of queries if its not set to paginate.  When pagination is set then it will restrict records to the perPage total.\n\nWe use get() just like laravel, when we need to filter, order or paginate.\n\n### Result Set Functions\n\nAs noted result sets are just like laravel collections, with additional functions.\n\nSo if our API only returns 30 results per page, our all method will do its best to return as many results as it can.  This will generally lead to some pagination.  So to retrieve the next set of results we can do the following.\n\n```php\n    $meetings = $user-\u003emeetings;\n    // Do some logic and discover need more results\n    $meetings = $meetings-\u003enextPage();\n\n    // $meetings-\u003epreviousPage() will go back a page.\n\n    // We can also iterate directly over the returned results\n    foreach($meetings-\u003enextPage() as $meeting)\n\n    //Finally for those using json api in SPA app, you can utilise the toArray or toJson functions\n    $meetings-\u003etoArray();\n\n    // returns \n    array:5 [\n      \"current_page\" =\u003e 1\n      \"data\" =\u003e array:2 [\n        0 =\u003e array:10 [\n          // $attributes\n        ]\n        1 =\u003e array:10 [\n          // $attributes\n        ]\n      ]\n      \"last_page\" =\u003e 5\n      \"per_page\" =\u003e 30\n      \"total\" =\u003e 137\n    ]\n```\n\nThe previousPage() method will go back a page, we cache the results so going back will not make an api call but pull the cached results.\n\nSometimes you may also want to accumulate records, to do this you can call the getNextRecords() method.  Please note not to mix this method with the next and previous page methods.\n\n```php\n    // will add more records to the current record set. The amount of records retrieved is based on the maxQueries and per page methods.\n    $meetings-\u003egetNextRecords();\n```\n\n// Need to check this as think Xero doesn't return page counts.\nIf you try to retrieve more records than there is available, then an exception will be thrown.\n\n### Links\n\nAt present there is no facility to use predefined links but it is something we may add in the future.\n\n## Raw Searches\n\nIf you would like to receive the raw response from the query then set raw on the query\n\n```php\n\t$users = API::account()-\u003eraw()-\u003eall();\n\n\t$users = API::account()-\u003ewhere('Type', 'Admin')-\u003eraw()-\u003eget();\n```\n\n## Http Errors\n\nIf there are any errors returned from our call and the response is not set to raw then we will throw a new Http Exception.  We have some default behaviour but different APIs have different responses to errors.  So you can override the prepareHttpErrorMessage() method on the builder model to customise the Exception message.\n\n```php\n\t$json = $response-\u003ejson();\n\tif($json['Type'] == 'ValidationException'){\n\t\t$message = $json['Message'];\n\t\tforeach($json['Elements'][0]['ValidationErrors'] as $error){\n\t\t\t$message .= ' : '.$error['Message'];\n\t\t}\n    \treturn $message;\n\t} else {\n    \treturn $json['Message'];\n\t}\n```\n\n## Models\n\nWe have 2 base model types, resources and apiResources.\n\nWe utilise Laravel's hasAttributes trait in the models so you should be able to use any casts like Laravel in any model.\n\n### Resources\n\nThese are generally resources that are returned as relationships of other models but do not interact with the API directly.  To create one extend the MacsiDigital/API/Support/Resource.\n\nThis should only be used on models that are returned as a sub array of a called model.\n\n### API Resources\n\nThese are models that will interact directly with an API, or indirectly through a parent model. To create one extend the MacsiDigital/API/Support/APIResource.\n\nIf you want to roll your own then you need to ensure you add the MacsiDigital\\API\\Traits\\InteractsWithAPI trait.  Also follow how our Support API resource works.\n\nIn these models we also house many variables, like primary key and api endpoints, these are the available attributes\n\n```php\n\t// These will map to RESTful requests\n\t// index -\u003e get and all\n\t// create -\u003e post\n\t// show -\u003e first\n\t// update -\u003e patch or put\n\t// delete -\u003e delete\n    protected $allowedMethods = ['index', 'create', 'show', 'update', 'delete'];\n\n    protected $endPoint = 'user';\n\n    protected $updateMethod = 'patch';\n\n    protected $storeResource;\n    protected $updateResource;\n\n    protected $primaryKey = 'id';\n\n    // Most APIs return data in a data attribute.  However we need to override on a model basis as some like Xero return it as 'Users' or 'Invoices'\n    protected $apiDataField = 'data'; \n\n    // Also, some APIs return 'users' for multiple and user for single, set teh multiple field below to wheat is required if different\n    protected $apiMultipleDataField = 'data'; \n```\nThe apiData and apiMultiple fields dictate what field in the response will house the main body data, this should be 'data' in a good api but it really does vary.  Zoom uses 'users' for multiple records and '' for single.\n\nIf apiDataField is set to '' it will return the body direct.\n\n```php\n    protected $apiDataField = '';\n```\n\nXero also uses a different method.\n\nIn the case of Xero it can be overwritten in the getApiMultipleDataField function so that we don't have to set on all models.\n\n```php\n\tpublic function getApiMultipleDataField()\n    {\n    \t// Xero uses pluralised end points like 'Users' so we can use this to pick the data from responses\n        return $this-\u003eendPoint;\n    }\n```\n\nIn a similar way we can override the primaryKey as some api's will keep the ID field as UserID by creating the following function\n\n```php\n\tpublic function getKeyName()\n    {\n        return $this-\u003eendPoint.'ID';\n    }\n```\n\n### EndPoints\n\nA quick note on endpoints, we can set an endpoint as 'users' but we can also include bound models similar to a laravel route when results are returned as part of a relationship.\n\n```php\n    protected $endPoint = 'users/{user:id}/settings';\n```\n\nWe can also set customEndpoints on a model for specific endPoints, if the endPoints don't follow convention.\n\n```php\n    protected $customEndPoints = [\n        'get' =\u003e 'users/{user:id}/meetings',\n        'post' =\u003e 'users/{user:id}/meetings'\n    ];\n```\n\n### Relationships\n\nWe have tried to get the models as close to a Laravel model as we can, even down to how relationships work.\n\nBy default we will try to create relationship objects for any returned input if they are setup. However you can override this behaviour by setting LoadRaw to true in the model\n\n```php\n\tprotected $loadRaw = false;\n```\n\nThere may be times where you want some auto-loading but not allow all connected models to autoload, in this case you can set any models that should not autoload by setting them like so:-\n\n```php\n\tprotected $dontAutoloadRelation = ['Address'];\n```\n\nFor each model we need to create a function, just like Laravel, so if there is a User Model which has a relationship with an Address Model you would set:-\n\n```php\n\tpublic function address() \n    {\n    \treturn $this-\u003ehasOne(Address::class);\n    }\n```\n\nThis could also be a HasMany relationship and the reverse on the address would be a belongsTo method\n\n```php\n\tpublic function user() \n    {\n    \treturn $this-\u003ebelongsTo(User::class);\n    }\n```\n\nA name and a field can be passed as a 2nd and 3rd argument.  A 4th argument can also passed which will be any fields and values in an array that should be passed onto all relationship models.  This is handy when you need to track a parent's id on teh child model.\n\nWe try to automatically work out the name and field attributes if not passed for you based on the function name, so we will look for a field 'user_id' in the array 'users' in the above method. However not all APIs use the same id naming so you can set the IDSuffix by adding this to your model.\n\n```php\n\tprotected $IdSuffix = 'ID';\n\n\t// Will now look for userID instead of user_id\n```\n\nWith the name field we will check results for User and user\n\nIts worth pointing out that this is case sensitive so User and user will give different results, UserID and userID.  Again this is due to all APIs being different.\n\nIts also worth pointing out that some resources don't interact directly with the API, in these cases the field is ignored.\n\nAt present we do not have the ability for Many to Many but will see if there is a need in our API building quests.\n\nNow that the relationships are set we can use Laravel like syntax to retrieve and work with models and relationships\n\n```php\n\t$user = API::user()-\u003efind('id');\n\n\t$address = $user-\u003eaddress;\n\n\t// or you can also call the method for further filtering\n\t\n\t$address = $user-\u003eaddress()-\u003ewhere('type', 'billing')-\u003efirst();\n```\n\nWe utilise save, saveMany (has many only), create and createMany (has many only) functions to save existing models and create new models directly to the relation, as long as they are models that interact with the api.\n\n```php\n    // save\n\n    $user = API::user()-\u003efind('id');\n\n    $address = Api::address()-\u003efind('id');\n    \n    $user-\u003eaddress()-\u003esave($address);\n\n    // create\n\n    $user = API::user()-\u003efind('id');\n    \n    $user-\u003eaddress()-\u003ecreate([\n        'address1' =\u003e '17 Test Street',\n        ...\n    ]);\n```\n\nWhen the relation models do not interact with the api then we expose new make() and attach() methods so they can be attached to persisting models for saving.\n\n```php\n    $user = API::user()-\u003efind('id');\n    \n    $user-\u003eaddress()-\u003emake([\n        'address1' =\u003e '21 Test Street',\n        ....\n    ]);\n\n    $user = API::user()-\u003efind('id');\n\n    $address = Address::make([...]);\n\n    // Do some logic checks\n    \n    $user-\u003eaddress()-\u003eattach($address);\n```\n\nThis works well when relationships are returned direct as part of an API call, which is common in APIs.  However sometimes APIs don't send these items direct and therefore need different end point calls.\n\nIn these cases we use custom relationship models\n\n### Custom Relationship Models\n\nFirst you need to extend either the HasOne or HasMany relationship models.\n\nWithin the extended model you need to create the logic for retrieving, creating, updating and deleting as is required by the API.  Unfortunately as these are all case specific you will need to create CustomRelations models for any implementation required.\n\nTo call it we call the hasCustom method and pass the model class as the 2nd parameter.  Parameters 3 and 4 can still be the name and field and 5 any fields and values to add to any new models.\n\n```php\npublic function addresses()\n{\n    $this-\u003ehasCustom(Address::class, addressHasMany::class);\n\n}\n```\n\nThe custom class will need a constructor, a save, saveMany (for hasMany), create, createMany (for hasMany) and getResults methods.\n\nThis really gives a blank canvas to be able to get those pain in the butt endpoints into your API.\n\nSometimes relationships are not returned as part of the request and have to be called separately.  Sometimes this may mean hitting a different endpoint, which is set in the model, see note on endPoints above.\n\nWe can also override the default with customEndPoints on a per model basis.\n\n```php\n    protected $customEndPoints = [\n        'get' =\u003e 'users/{user:id}/meetings',\n        'post' =\u003e 'users/{user:id}/meetings'\n    ];\n```\n\nYou can set end points for find, get, create, update and delete.\n\n## Filters\n\nFilters will vary from API to API, so we have tried to build an extendable filter functionality that can easily be implemented.\n\nFilters use Laravel like syntax to apply with the where functions\n\n```php\n\t$user = API::user()-\u003ewhere('Name', 'John Doe')-\u003efirst();\n\n\t$users = API::user()-\u003ewhere('Name', '!=', 'John Doe')-\u003eget();\n\n\t$posts = API::post()-\u003ewhere('Title', 'like', 'Mechanical')-\u003eget();\n```\n\nAnd of course you can stack where clauses\n\n```php\n\t$posts = API::post()-\u003ewhere('created_at', '\u003e', '2019-01-01')-\u003ewhere('created_at', '\u003c', '2019-12-31')-\u003eget();\n```\n\nAt present we only cover the where and whereIn, the latter will only work with the ID field, but we intend to add more of the laravel where clauses, like whereBetween if we find there is support for this in APIs.\n\nNow as noted all APIs are different so generally we have to customise the filter logic.\n\nFirst of all you need to create a Builder model that extends the Support/Builder model.\n\nIn the Entry model you need to set the below function\n\n```php\n\tpublic function getBuilderClass() \n    {\n        return \\Namespace\\To\\Your\\Builder::class; \n    }\n```\n\nIn this builder class you would then create functions that are called when filters are added and when they are processed into the query string. So to modify we would modify 2 methods\n\n```php\n\tprotected function addWhereEquals($column, $operand, $value)\n    {\n        $this-\u003ewheres[] = ['column' =\u003e $column, 'operand' =\u003e $operand, 'value' =\u003e $value];\n    }\n\n    public function processWhereEquals($detail) // Passed in the array attached in above method\n    {\n        $this-\u003eprocessedWheres[$detail['column']] = $detail['value'];\n    }\n```\n\nAs arrays cant use symbols as keys we do some translating, so if '=' is called we will change this to AddWhereEquals and processWhereEquals, the list below is of the translations we make.\n\n```php\n\tcase '=':\n\t    return 'Equals';\n\tcase '!=':\n\t    return 'NotEquals';\n\tcase '\u003e':\n\t    return 'GreaterThan';\n\tcase '\u003e=':\n\t    return 'GreaterThanOrEquals';\n\tcase '\u003c':\n\t    return 'LessThan';\n\tcase '\u003c=':\n\t    return 'LessThanOrEquals';\n\tcase '\u003c\u003e':\n\t    return 'GreaterThanOrLessThan';\n\tcase 'like':\n\t    return 'Contains';\n\tdefault:\n\t    return 'Process'.Str::studly($operand);\n```\n\nThe default is called for any custom filters, so lets say you call where('name', 'StartsWith', 'Bob') this will call addWhereStartsWith and processWhereStartsWith methods\n\nNow each API will handle filtering differently so the logic in these methods are to suit the API, here is an example for Xero, who add all filters to a where query string.  They allow the main ones and have 3 custom types, 'Contains' which is the same as a 'like' call, 'StartWith' and 'EndsWith'.\n\n```php\n\tpublic function processEquals($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'==\"'.$detail['value'].'\"';\n\t\t\n\t}\n\n\tpublic function processNotEquals($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'!=\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processGreaterThan($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'\u003e\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processGreaterThanOrEquals($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'\u003e=\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processLessThan($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'\u003c\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processLessThanOrEquals($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'\u003c=\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processGreaterThanOrLessThan($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'\u003c\u003e\"'.$detail['value'].'\"';\n\t}\n\n\tpublic function processContains($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'.Contains(\"'.$detail['value'].'\")';\n\t}\n\n\tpublic function processStartsWith($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'.StartsWith(\"'.$detail['value'].'\")';\n\t}\n\n\tpublic function processEndsWith($detail) \n\t{\n\t\tif(!isset($this-\u003eprocessedWheres['where'])){\n\t\t\t$this-\u003eprocessedWheres['where'] = '';\n\t\t}\n\t\t$this-\u003eprocessedWheres['where'] .= $detail['column'].'.EndsWith(\"'.$detail['value'].'\")';\n\t}\n```\n\nAs we create custom filter methods, this makes filtering easy and extremely powerful, but it really does depend on what the API allows to be done.\n\nSo as an example in Xero we can apply a If-Modified-Since header to our request to retrieve only models modified since a specific date, this can be achieved like so.\n\n```php\n\t// you would need to spoof the column name, as its not used but is still required. \n\t// So call something like\n\t$user-\u003ewhere('UpdatedDate', 'ModifiedAfter', $date)-\u003eget();\n\n\tpublic function addWhereModifiedAfter($column, $operand, $value) \n\t{\n\t\t$this-\u003ewheres[] = ['operand' =\u003e $operand, 'value' =\u003e $value];\n\t}\n\n\tpublic function processWhereModifiedAfter($detail) \n\t{\n\t\t$this-\u003erequest-\u003ewithHeader([\n\t\t\t'If-Modified-Since' =\u003e $detail['value']\n\t\t]);\n\t}\n```\n\nOf course for these sorts of custom actions we have to update the allowed operands field within our Entry model to allow them to be applied.\n\n```php\n\tprotected $allowedOperands = ['=', '!=', '\u003c', '\u003e', '\u003c=', '\u003e=', '\u003c\u003e', 'like', 'ModifiedAfter'];\n```\n\n## Creating and Updating\n\nGenerally in APIs the attributes required for saving and updating are different, and they are in turn different to the attributes when a model is retrieved.  We therefore utilise 'Persistence' models which will house validation logic and the attributes required to make a successful save.  So in our models we need to Extend the InteractsWithAPI trait and add the following 2 protected attributes.\n\n```php\n\tprotected $insertResource = 'MacsiDigital\\API\\Dev\\Resources\\StoreAddress';\n    protected $updateResource = 'MacsiDigital\\API\\Dev\\Resources\\UpdateAddress';\n```\n\nThe Insert and Update resources should extend the MacsiDigital/API/Support/PersistResource.  These models will have attributes for what fields to use and any validation.\n\n```php\n\tprotected $persistAttributes = [\n    \t'name' =\u003e 'required|string|max:255',\n    \t'email' =\u003e 'required|email|string|max:255',\n    \t'password' =\u003e 'required|string|max:10',\n    ];\n```\nAs you can see we set the field and any regular Laravel validation logic in the persistAttribute, if there is no validation we can pass ''.\n\nWe can also extend this to include relationships.  It may look like so:-\n\n```php\n\tprotected $persistAttributes = [\n    \t'name' =\u003e 'required|string|max:255',\n    \t'email' =\u003e 'required|email|string|max:255',\n    \t'password' =\u003e 'required|string|max:10',\n    ];\n\n    protected $relatedResource = [\n    \t'address' =\u003e '\\MacsiDigital\\API\\Dev\\Resources\\UpdateAddress'\n    ];\n```\n\nWhen utilising the relationships in this way it will dip into the linked resource model and use its validation and fields logic. So in this it will pull in the following logic\n\n```php\n\tprotected $persistAttributes = [\n    \t'street' =\u003e 'required|string|max:255',\n    \t'address2' =\u003e 'nullable|string|max:255',\n    \t'town' =\u003e 'required|string|max:255',\n    \t'county' =\u003e 'nullable|string|max:255',\n    \t'postcode' =\u003e 'required|string|max:10',\n    ];\n\n```\n\nWe can pass an array directly into the relatedResource attribute if we don't want to create a persistent resource.\n\n```php\n    protected $relatedResource = [\n    \t'address' =\u003e [\n            'street' =\u003e 'required|string|max:255',\n    \t\t'address2' =\u003e 'nullable|string|max:255',\n    \t\t'town' =\u003e 'required|string|max:255',\n    \t\t'county' =\u003e 'nullable|string|max:255',\n    \t\t'postcode' =\u003e 'required|string|max:10',\n        ],\n    ];\n```\n\nWe can require fields from relationships by using dot notation and not including the relationship:-\n\n```php\n\tprotected $persistAttributes = [\n    \t'name' =\u003e 'required|string|max:255',\n    \t'email' =\u003e 'required|email|string|max:255',\n    \t'password' =\u003e 'required|string|max:10',\n    \t'address.street' =\u003e 'required|string|max:255',\n    \t'address.town' =\u003e 'required|string|max:255',\n    \t'address.postcode' =\u003e 'required|string|max:10',\n    ];\n```\n\nSo in this instance we are only interested in street, town and postcode from the address model.\n\nWe can use a mix of the above, its not one or the other.\n\n### Mutating\n\nSome APIs will mutate the data, a common case is wrapping the fields in a new array.  \n\n```php\n    //Normal create\n   [\n        'name' =\u003e 'John Doe',\n        'email' =\u003e 'john@Example.com'\n   ] \n\n   // Some APIs may require\n   // \n   [\n        'action' =\u003e 'create',\n        'user_info' =\u003e [\n            'name' =\u003e 'John Doe',\n            'email' =\u003e 'john@Example.com'\n        ]\n   ]\n```\n\nTo achieve this we can use Mutators on the persistModel\n\n```php\n    protected $persistAttributes = [\n        'action' =\u003e 'required|string|in:create,autoCreate,custCreate,ssoCreate',\n        'user_info.name' =\u003e 'required|string|max:255',\n        'user_info.email' =\u003e 'required|email|string|max:255',\n    ];\n\n    // To apply mutators we set what teh normal key is and what we want it to end up as\n    protected $mutateAttributes = [\n        'name' =\u003e 'user_info.name',\n        'email' =\u003e 'user_info.email'\n        'type' =\u003e 'status' //can also use to mutate current attributes into attributes for the api\n    ] \n\n```\n\nThis will now create the correct json for the api and allow validation to still take place.\n\n## Saving\n\nTo save its as simple as calling the save() function.\n\n```php\n\t$user-\u003esave();\n```\n\nTHe model will see if it already exists and call the correct insert or update method.  If updating we will only pass dirty attributes to the persistence model.\n\nYou can also utilise other laravel methods like make, create and update directly in the model.\n\nWe pass back helpful exceptions if it fails due to the API rejecting it. For example some APIs will have unusual rules, in Xero you can only pass multiple contacts if you supply email addresses, in this case we would get back an exception:-\n\n```bash\nMacsiDigital/API/Exceptions/HttpException with message 'A validation exception occurred : Additional people cannot be added when the primary person has no email address set.'\n```\n\n## Deleting\n\nTo delete we would just call the delete function, this will return true if it deleted or throw a HttpException if there was an error.\n\n```php\n\t$user-\u003edelete();\n```\n\n## Calling custom requests\n\nWhilst we do our best to cover the main restful endPoints for models, there are going to be some cases where we have to create something custom.\n\nWe have tried to make that as easy as possible, each model has the API client injected in on instantiation, so we can create any function to call on the API.  For example, zoom exposes an endPoint to upload a profile picture, we could create persistModels, but it may be overkill, so we can simply create a function like so\n\n```php\n    public function updateProfilePicture($image)\n    {\n        $filesize = number_format(filesize($image) / 1048576,2);\n        if($filesize \u003e 2){\n            throw new FileTooLargeException($image, $filesize, '2MB');\n        } else {\n            return $this-\u003enewQuery()-\u003eattachFile('pic_file', file_get_contents($image), $image)-\u003esendRequest('post', ['users/'.$this-\u003eid.'/picture'])-\u003esuccessful();\n        }\n    }\n```\nNice and simple.\n\n## ToDo\n\n- Proper Documentation\n- Tests\n\n## Changelog\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\n## Contributing\n\nPlease see [CONTRIBUTING](CONTRIBUTING.md) for details.\n\n## Security\n\nIf you discover any security-related issues, please email [info@macsi.co.uk](mailto:info@macsi.co.uk) instead of using the issue tracker.\n\n## Credits\n\n- [Colin Hall](https://github.com/colinhall17)\n- [MacsiDigital](https://github.com/MacsiDigital)\n- [All Contributors](../../contributors)\n\nWe use a lot of Laravel type functions taken directly from Laravel, or taken and modified so we also have to credit the Laravel team.\n\n- [Laravel](https://github.com/laravel)\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmacsidigital%2Flaravel-api-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmacsidigital%2Flaravel-api-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmacsidigital%2Flaravel-api-client/lists"}