/willow

API Response Factories for Laravel

Primary LanguagePHPMIT LicenseMIT

willow

API Response Factories for Laravel

Target API

This section defines targets for how the library should be used when the project is finished.

Responses

This is the API for actually creating fake external API responses.

/**
 * GET Requests
 */

// Get a generated fake resource from requesting a single resource from an API.
WillowFactory::fakeApi()->show();
// Get a generated fake collection from requesting an index of resources from an API.
WillowFactory::fakeApi()->count(3)->index();
// Override specific fields as they are generated by the factory.
WillowFactory::fakeApi()->show([
    'foo' => 'bar',                         // overrides field `foo` to always be "bar"
    'person.name' => 'Montgomery Scott',    // nested fields use same syntax as data_set()
]);

// Add lifecycle functions to transform the data after it is generated.
WillowFactory::fakeApi()->count(3)->afterMaking(function(array $data) { // runs after each resource is made
    $data['value']++;
})->index();
WillowFactory::fakeApi()->count(3)->afterMaking(function(array $data) {
    $data['value']++;
})->afterComposing(function(array $data) {  // runs after the whole response is generated
    data['status'] = 200;
})->index();

/**
 * PUT/PATCH/POST/DELETE Requests
 * PUT functions about the same as POST, but the assumption is the resource will
 * have been updated rather than created. This may alter the response object, but
 * does not alter the API used to interact with the factory.
 *
 * Patch is a synonym for put in this case.
 *
 * Delete is also just a type of update, and consequently also uses the same API.
 * Just like with put or post, the different method call merely allows the user to
 * define seperate make/compose methods for generating the response. Otherwise it
 * is identical API and behavior behind the scenes.
 */

// Get a response based on creating a resource without specifying request data.
WillowFactory::fakeApi()->post();
WillowFactory::fakeApi()->put();
WillowFactory::fakeApi()->patch();
WillowFactory::fakeApi()->delete();
// Manually specify data to pass with request.
WillowFactory::fakeApi()->requestData([
    'fname' => 'John',
    'lname' => 'Appleseed',
    'age' => 28,
])->post();
WillowFactory::fakeApi()->requestData(['id' => 10, 'name' => 'John Appleseed'])->put();
WillowFactory::fakeApi()->requestData(['id' => 10, 'name' => 'John Appleseed'])->patch();
WillowFactory::fakeApi()->requestData(['id' => 10, 'name' => 'John Appleseed'])->delete();
// Use a request data factory (in this case called `person`) to generate data to pass with request.
WillowFactory::fakeApi()->count(3)->sendsPerson()->post();
WillowFactory::fakeApi()->count(3)->sendsPerson()->put();
WillowFactory::fakeApi()->count(3)->sendsPerson()->patch();
WillowFactory::fakeApi()->count(3)->sendsPerson()->delete();

// Can use lifecycle hooks with any kind of response.
WillowFactory::fakeApi()->count(3)->sendsPerson()->afterMaking(function(array $data) {
    $data['value']++;
})->post();

// Can force a fail response from any action.
WillowFactory::fakeApi()->fail()->show();
WillowFactory::fakeApi()->fail()->sendsPerson()->delete();

Factory Definition

This is how to define a response factory that can be consumed by the API specified above.

<?php

namespace Database\Factories\Api\Responses;

use Willow\Factory;

class FakeApi extends Factory
{
    /**
     * The name that will be used to access this factory from the Willow facade.
     * The one shown here will be the default unless overridden here.
     * @var string
     */
    protected static $accessor = 'fakeApi';

    /**
     * By default the factories return an array. If this is set to true (or the default is changed
     * in the config) then the array will be json encoded before beign returned.
     * @var bool
     */
    protected static $asJson = false;

    /**
     * Determines how the API response data should be formatted before
     * being returned.
     */
    public function compose(array $generated): array
    {
        return [
            'status' => $this->autoStatus,  // Base the status code on the request method used.
            'response' => $generated,       // The collection/resource
        ];
    }

    /**
     * You can create a composition for a specific method insead of using the default.
     */
    public function deleteCompose(array $generated): array
    {
        return [
            'status' => 204,
            'message' => 'resource has been deleted',
        ];
    }

    /**
     * This is used when defined if the user specifies the API should return an error response.
     * This allows the user to
     */
    public function failed(array $generated): array
    {
        return [
            'status' => 503,
            'message' => 'Service unavailable',
        ];
    }

    /**
     * Can override the default failure response for a specific request method.
     */
    public function deleteFailed(array $generated): array
    {
        return [
            'status' => 503,
            'message' => 'Service unavailable',
            'id' => 2001,
        ];
    }

    /**
     * Defines how each resource from the API should be constructed. Could be modified to also
     * just return an instance of a request data factory, to use that definition instead.
     */
    public function definition(): array
    {
        return [
            'quote' => $this->faker->bs(),  // The class has a faker instance.
            'source' => [
                'name' => $this->faker->name(),
                'age' => rand(21, 65),
            ]
        ];
    }

    /**
     * You can create a definition for a specific action to use instead of the default.
     */
    public function postDefinition(): array
    {
        return [
            'quote' => $this->faker->bs(),  // The class has a faker instance.
            'source' => [
                'name' => $this->faker->name(),
                'age' => rand(21, 65),
            ]
        ];
    }
}

Request Data Factory

Request Data Factories are simple classes. They function like an API factory, but have no lifecycle hooks, counts, or different methods for accessing. They merely have a definition method and Faker instance.

Potentially add a field that allows the user to make an 'id' field optional. This likely won't actually be needed, request data for mocked calls rarely needs to be that accurate, but POST requests usually won't want an ID, but PUT/DELETE does. Maybe nothing that specific, maybe allow the user to mark any field as optional and just pass in an array of optional fields to include. But again, this is probably overthinking things. This data doesn't actually get sent anywhere, it just gets spat back out at the user.

<?php

namespace Database\Factories\Api\Requests;

use Willow\RequestDataFactory;

class Person extends RequestDataFactory
{
    /**
     * Defines how to generate an API request or response representing a model. Can
     * be used for generating POST requests or even used as the definition of an API
     * factory.
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'age' => rand(21, 65),
        ];
    }
}

Artisan Commands

The following artisan commands should be supported for managing factories.

Create a new factory:

$ php artisan willow:make FakeApi/Person
$ php artisan willow:touch fake-api.person

Create a factory with stubs for specific actions:

$ php artisan willow:make FakeApi/Person --crud
Create custom actions stubs for show(r), post(c), put(u), and delete(d)
$ php artisan willow:touch fake-api.person --d
Create custom action stub for delete(d)

Move an existing factory:

$ php artisan willow:rename FakeApi/Person FakeApi/User
$ php artisan willow:mv fake-api.person fake-api.user

Delete a factory:

$ php artisan willow:delete FakeApi/Person
$ php artisan willow:rm fake-api.person

Config

Default config file

<?php

return [
    'locations' => [
        'api' => '',        // default location for api factories
        'request' => '',    // default location for request data factories
        // TODO: I thought this was a thing that is done, but it doesn't even really seem possible.
        // We would need to update the PSR in the composer json to reference a different namespace, otherwise it just kind of has to live with the factories--or somewhere in the db namespace.
    ],

    'asJson' => false,      // whether or not response objects should be encoded as json
];

Roadmap

  • Create parent class that can be used to define factories
  • Create API for basic featureset in generating factories
  • Create a Willow facade that can be used to access factories
    • Add and register facade
    • Add a method handler that will look for registered api factories and return an instance
  • Basic support for artisan commands
  • The ability to support publishing code stubs/config for overrides
  • Create parent for request data factories
    • Create class
    • Add support for using as api factory definition
    • Add a method handler to api factory that will attach a data factory
  • Add support for accessing a factory via different rest methods
    • show/index (index is likely just the default)
    • post
    • put/patch
    • delete
  • Configure github actions, contributing.md, branding, etc.
  • Finalize all tests and ensure full coverage
  • Tag and release version 1.0

Outstanding Questions

These are concepts that need further exploration.

  1. Is there a way to logically handle some of the more common requirements for tests? Like instead passing a date into a field when making a response, pass in a keyword like 'past' or 'future' so that it will only generate a date before or after 'now' respectively. This could help when testing to make sure only the proper models show up.
  2. Is there a way to support some kind of relationships? eg. including Laravel Models or Request Data Factories.
  3. Add the ability to specify the API in a YAML format a la something like smocker?
  4. Could you build a livewire/controller trait that could be used as kind of a factory that grabs data from an external api when live, but fetch from factory when debug mode is on.