Git Product home page Git Product logo

api's Introduction

Aye Aye API

[PHP >= 5.5] (https://php.net/) [License: MIT] (https://raw.githubusercontent.com/AyeAyeApi/Api/master/LICENSE.txt) [Version] (https://packagist.org/packages/ayeaye/api) [Build Status] (https://travis-ci.org/AyeAyeApi/Api/branches)

Aye Aye API is a micro framework for building API's written in PHP. It's designed to be easy to use, fast to develop with and to scale from tiny micro-services to world devouring titans.

Installation

Create a project and include Aye Aye.

composer init --require="ayeaye/api ^1.0.0" -n
composer install

Quick Start Guide

When working with Aye Aye, you will do almost all of your work in Controller classes.

Here's our ubiquitous Hello World controller:

<?php
// HelloWorldController.php

use AyeAye\Api\Controller;

class HelloWorldController extends Controller
{
    /**
     * Yo ho ho
     * @param string $name Optional, defaults to 'Captain'
     * @return string
     */
    public function getAyeAyeEndpoint($name = 'Captain')
    {
        return "Aye Aye $name";
    }
}

Controllers contain endpoints and child controllers. The above controller has a single endpoint hello that will respond to HTTP GET requests. This is reflected in the name which takes the form [verb][Name]Endpoint.

The endpoint takes one parameter, name, which will default to 'Captain' if not otherwise provided. The return is a string.

The API needs an entry point, which will put in index.php

<?php
// index.php

require_once '../vendor/autoload.php';
require_once 'HelloWorldController.php';

use AyeAye\Api\Api;

$initialController = new HelloWorldController();
$api = new Api($initialController);

$api->go()->respond();

First we grab composer's autoloader, and our controller (which we haven't added to the autoloader). We instantiate our HelloWorldController, and pass it into the constructor of our Api object. This becomes our initialController, and it's the only one Aye Aye needs to know about, we'll come onto why later.

Finally the ->go() method produces a Response object, with which we can ->respond().

We can test this using PHP's build in server:

$ php -S localhost:8000 index.php &

$ curl localhost:8000/aye-aye                 # {"data":"Aye Aye Captain"}
$ curl localhost:8000/aye-aye?name=Sandwiches # {"data":"Aye Aye Sandwiches"}

Notice how the string has been converted into a serialised object (JSON by default but the format can be selected with an Accept header or a file suffix).

That tests our endpoint, but what happens if you just query the root of the Api.

$ curl localhost:8000 # {"data":{"controllers":[],"endpoints":{"get":{"aye-aye":{"summary":"Yo ho ho","parameters":{"name":{"type":"string","description":"Optional, defaults to 'Captain'"}},"returnType":["string"]}}}}} 

Lets take take a closer look at that returned string.

{
  "data": {
    "controllers": [],
    "endpoints": {
      "get": {
        "aye-aye": {
          "summary": "Yo ho ho",
          "parameters": {
            "name": {
              "type": "string",
              "description": "Optional, defaults to 'Captain'"
            }
          },
          "returnType": [
            "string"
          ]
        }
      }
    }
  }
}

As you can see, it is an explanation of how our controller is structured. We didn't write anything more than what was expected of us, and it makes sense to both the back end developers, and the consumers of your Api.

Don't forget to close the server down when you're done.

$ fg
^C

Why should you use it?

Developing in Aye Aye is simple, clean and logical. Aye Aye processes requests and gives them to the appropriate endpoint on the appropriate controller. That endpoint is simply a method, that takes a set of parameters and returns some data. Aye Aye will work out where to find those parameters in the request, and it will format the data on return. It even supports multiple data formats and will automatically switch based on what the user requests.

There's no fluff. You don't need to learn new database tools, or logging interfaces (assuming you know [PSR-3] (https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md), and you should) or authentication methods. Aye Aye only provides routing, request parsing and response handling. You can use whatever you like for the rest.

If you follow PSR-4, then your API will look a lot like your directory structure, making maintenance a breeze.

Aye Aye knows about itself. It knows what endpoints and what sub-controllers are available on any given controller, and by reading the documentation in the doc-block comments, it can tell users what those end points do. You only need to write your documentation once, and Aye Aye will read it and tell your users what those end points do, and what parameters they take.

By default it can read and write data as json (the default) and xml, though more formats can easily be added. It also reads GET, POST and HEADER, and parametrises url slugs. Data from these sources is passed into your methods for you.

Quick Start explained

The most important and powerful feature of Aye Aye is it's controllers. Controllers do two things. They provide endpoints, and access to other controllers.

Controllers are classes that extend AyeAye\Api\Controller

Endpoints are methods on the controllers that are named in this way [verb][name]Endpoint(...)

  • The [verb] is the http verb, the endpoint is waiting for.
  • The [name] is the name of the endpoint.
  • Endpoint is literally the word "Endpoint". It helps us know what we're dealing with.

You can define any parameters you like for the method, and Aye Aye will automatically populate them for you.

Controllers can also reference other controllers with methods named like this [name]Controller()

These should return a controller object, and it's how Aye Aye navigates the Api.

The hello world controller

Above we wrote a controller to say hello.

<?php
// HelloWorldController.php

use AyeAye\Api\Controller;

class HelloWorldController extends Controller
{
    /**
     * Yo ho ho
     * @param string $name Optional, defaults to 'Captain'
     * @return string
     */
    public function getAyeAyeEndpoint($name = 'Captain')
    {
        return "Aye Aye $name";
    }
}

The one method in this controller tells you everything you need to know.

  • It will respond to a GET request send to the hello endpoint.
  • It takes one parameter, 'name', which will default to Captain
  • It returns a string

So how did we go from that, to sending and receiving the data with curl?

When we created the Api object, we passed it a HelloWorldController object as a parameter, this tells the Api this is our starting point. The Aye Aye identifies getAyeEndpoint as an endpoint called "aye" that is triggered with a GET request.

You'll notice that we used a PHP Doc Block to explain what the method does. This is really important. Not only does it tell other developers what this end point does... it tells your API's users too, and they'll be using it in just the same way.

In the quick start guide we queryied "/", and you will have seen that the Api tells you it has one GET endpoint, called 'aye, that it takes one parameter, as string called name, and it described all of these things with the documentation you made for the method!

Child Controllers

Obviously just having one controller is pretty useless. To go from one controller to the next, we create a [name]Controller() method. This method should return another object that extends Controller. To demonstrate that in our application quick start application, we can just return $this.

<?php
// HelloWorldController.php

use AyeAye\Api\Controller;
 
class HelloWorldController extends Controller
{
    /**
     * Yo ho ho
     * @param string $name Optional, defaults to 'Captain'
     * @returns string
     */
    public function getAyeAyeEndpoint($name = 'Captain')
    {
        return "Aye Aye $name";
    }
    
    /**
     * lol...
     * @returns $this
     */
    public function ayeController()
    {
        return $this;
    }
}

Now when we start our application and the fun begins!

$ php -S localhost:8000 public/index.php &
curl localhost:8000/aye/aye/aye/aye/aye-aye?name=Aye%20Aye # {"data":"Aye Aye Aye Aye"}

Contributing

Aye Aye is an Open Source project and contributions are very welcome.

Issues

To report problems, please open an Issue on the GitHub Issue Tracker.

Changes

Send me pull requests. Send me lots of them.

We follow the PSR-1 and PSR-2 coding standards. PHPMD and PHPCS, and their rule files will help guide you in this.

api's People

Contributors

gisleburt avatar harkinwebb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

api's Issues

Simplify method hiding.

Currently there's two sets of methods that do almost the same thing:

private $hiddenEndpoints = ['getIndexEndpoint'];
private $hiddenControllers = [];
protected function hideEndpointMethod($methodName);
public function isEndpointMethodHidden($methodName);
protected function showEndpointMethod($methodName);
protected function hideControllerMethod($methodName);
public function isControllerMethodHidden($methodName);
protected function showControllerMethod($methodName);

If we simplify this to just any methods, this makes things a lot easier.

private $hiddenMethods = ['getIndexEndpoint'];
protected function hideMethod($methodName);
public function isMethodHidden($methodName);
protected function showMethod($methodName);

Now it doesn't matter if it's an endpoint, controller, or something else we haven't thought of.

Use @hidden in method documentation instead of controller arrays

Currently you can choose to hide child controllers and endpoints by using arrays in the controller. These aren't very friendly. As we're already using PHPDoc comments to show information to end users, we can also use it to hide it, by using something like an @hidden flag. Eg.

/**
 * This endpoint is hidden
 * @hidden
 */
public function getExampleAction() {...}

This can be extended to controllers once Issue #2 is completed.

Aye Aye serializer or return wrapper

Currently the best way to prepare objects for serialization by Aye Aye's formatters is to use the JsonSerializable interface. If we swap this for an Aye Aye serializer or encourage the use a specific class for returning data, we can add in additional meta information.

This could be useful for solving #15 and #16.

Automated HTML documenter

As AyeAye self documents anyway, we could add an AJAX'd html skin to it. This could even be used to test calls quickly.

Exception Class Naming Semantics

Current Exception and \Exception is a little confusing to read

try{
//Blah
} catch (Exception $e) {
//Blah
} catch (\Exception $e) {
//Blah
}

This might read better

try{
//Blah
} catch (AyeAyeException $e) {
//Blah
} catch (\Exception $e) {
//Blah
}
`

If I then do this

class IansApiBuiltOnAyeAyeException extends AyeAyeException{}

//It reads nicely where the exception has been thrown from
try{
//Blah
} catch (IansApiBuiltOnAyeAyeException $e) {
//Blah
}
} catch (AyeAyeException $e) {
//Blah
} catch (\Exception $e) {
//Blah
}

Do not cast data to array before putting it into ayeAyeDeserialize

Assume the user and developer know best and don't cast data going into the method.

$value = $reflectionParameter->getClass()
    ->newInstanceWithoutConstructor()
    ->ayeAyeDeserialize((array)$value); // Remove array cast

The contents of $value can be adequately controlled by the new Readers in the Formatting module.

.gitattributes export-ignore

Certain files should be listed as export ignore (tests, QA tools, etc) to reduce the size of the "dist" package.

Interfaces

All classes should be derived from, and type hint with Interfaces to make replacement easier.

Rename actions -> endpoints, children -> controllers

Currently the code refers to controllers and children, which are the same thing, as well as actions and endpoints which are the same. Renaming them will make the code simpler to follow.

The decision is to go with "controllers" and "endpoints".

This will break all current implementations of the API but will be worth it in the long run.

Cache all the things

The main (only?) down side to using Aye Aye is it's speed. It's not horrific, but it's not competative with other API frameworks.

Would it be possible to cache documentation and complex routes to avoid using reflection and thus improve speed?

Allow other ways to separate words in controllers and end points

Currently you separate words with a hyphen. Eg getUserNameAction would be user-name. However, not all systems use this as the way to separate words. For example, PHP's urlencode() function uses +. Browsers will use %20.

This could be added to Controller::parseActionName(). This should be completed after #2 as it will add a parser for child controllers.

Unsatisfactory XML deserialization

Xml deserialization does not behave similarly to other objects such as json. You can not easily navigate a collection of SimpleXml objects which breaks the expectation of behaviour.

Make children a method

Each child can be a method that returns an instance of the correct controller. This should take the form

public function [name]Controller {...}

This will give us much better controller of when to show certain controllers, and prevent hiding functionality in variable strings.

Check Deserializable objects return an instance of themselves.

The Router will create instances of objects that implement the Deserilizable interface by calling their ayeAyeDeserialize method. This method MUST return an instance of the required class, otherwise the api will crash when it calls the endpoint with invalid data.

In Router::getParametersFromRequest add a check:

if(
    $reflectionParameter->getClass() &&
    $reflectionParameter->getClass()->implementsInterface('\AyeAye\Formatter\Deserializable')
) {
    /** @var Deserializable $deserializable */
    $value = $reflectionParameter->getClass()
                                 ->newInstanceWithoutConstructor()
                                 ->ayeAyeDeserialize((array)$value);
    $className = $reflectionParameter->getClass()->getName();
    if(!is_object($value) || !get_class($value) == $className)
    {
        throw new \RuntimeException(
            "{$className}::ayeAyeDeserialize did not return an instance of itself"
        );
    }
}

Exception chaining security hole

Exception::jsonSerialize() doesn't check the child exception is an AyeAye Exception, and could therefore end up revealing undesirable information.

Consider changing to this:

    /**
     * Return data to be serialised into Json
     * @return array
     */
    public function jsonSerialize()
    {
        $serialized = [
            'message' => $this->getPublicMessage(),
            'code' => $this->getCode(),
        ];
        $class = __CLASS__;
        if($this->getPrevious() instanceof $class) {
            $serialized['previous'] = $this->getPrevious();
        }
        return $serialized;
    }

Changes to doc-block reader break when not all information is present.

The changes introduced by fa49de3 result in broken documentation if not all data is present (i.e. it must be @param $property description, if description is missing it reads the next line).

New line issue

This is an area that's needed improving for sometime. The doc-block reader should roughly follow the proposed PSR-5

Notably the reader should:

  • Ignore white space
  • Identify the start of new information using the @ symbol
  • Allow multi line documentation

This could be achieved by cleaning the data up before trying to parse it.

  • Removing ^\s*\s
  • Remove new lines and split before the @ symbols
  • Removing excess whitespace
  • Parsing data

Request::stringToObject security and legability.

A few improvements things:

  • Remove the option for PHP. If people really want to take that risk they can extend the Request class and add it themselves.
  • Remove the @ error suppression and use libxml_use_internal_errors which will store any errors for later examination with libxml_get_errors
  • Move assignment out of ifs, it's just good practice, makes it easier to read, doesn't make it any more computationally intensive.
    • Check if $xmlObject is an object as empty SimpleXML objects evaluate to false (even though empty objects don't anymore >.>)
/**
 * Tries to turn a string of data into an object. Accepts json, xml or a php serialised object
 * Failing all else it will return a standard class with the string attached to data
 * eg. $this->stringObject('fail')->body == 'fail'
 * @param string $string a string of data
 * @throws \Exception
 * @return \stdClass
 */
public function stringToObject($string)
{
    if (!$string) {
        return new \stdClass();
    }
    // Json
    $jsonObject = json_decode($string);
    if ($jsonObject) {
        return $jsonObject;
    }
    // Xml
    libxml_use_internal_errors(false);
    $xmlObject = simplexml_load_string($string);
    if (is_object($xmlObject)) {
        return $xmlObject;
    }
    libxml_use_internal_errors(true);

    $object = new \stdClass();
    $object->text = $string;
    return $object;
}

Marked here because I technically can't work on this during office hours but it's an issue I observed in the normal course of my work. :/

E2E Tests

An additional layer of tests would give higher confidence. By creating a set of End 2 End tests using a tool such as Behat we can also show people how Aye Aye works without people needing to read the tutorial.

[Insight] Source code should not contain TODO comments - in src/Documentation.php, line 167

in src/Documentation.php, line 167

TODO comments are left in the code when a feature (or a bug) isn't completely developed (or fixed). You should complete the implementation and remove the comment.

                preg_match('/([^$]+)?\$(\w+)(.+)?/s', $paramDoc, $documentation);
                list(/*ignore*/, $type, $name, $description) = $documentation;


                // Clean up description
                // ToDo: Got to be a better way than this
                $lines = preg_split("/((\r?\n)|(\r\n?))/", $description);
                foreach ($lines as $key => $value) {
                    $value = preg_replace('/\r/', '', $value);
                    $value = preg_replace('/^\s+\*/', '', $value);
                    $value = trim($value);

Posted from SensioLabsInsight

JSON fallback

When the requested data format is unknown, the api should return an error in JSON.

Tutorial

A comprehensive tutorial should exist before launch. This tutorial should demonstrate the construction of a fully functional microservice, such as an authentication server. This should replace the previous tutorials which are too basic and don't achieve anything.

HATEOAS - Should Aye Aye Support it?

http://en.wikipedia.org/wiki/HATEOAS

I'm not a major fan of Hateoas as with every request for data it additionally tells you how the API works (rather than the Aye Aye way of having you just ask the Api how it works and when you ask for data you just get data).

It might be really good to support this in OPTIONS requests, which would be a good match with #15

Aye Aye Exceptions do not set status codes in Api catch

Change

class Api {
    ....
    catch(Exception $e) {
        $response->setData($e->getPublicMessage());
        return $response;
    }
    ...
}

to

class Api {
    ....
    catch(Exception $e) {
        $response->setData($e->getPublicMessage());
        $response->setStatusCode($e->getCode());
        return $response;
    }
    ...
}

Cyclomatic Complexity

Currently Documentation::getDescription has a cycolmatic complexity of 12. The mess detector rules say this must be below 10

    /**
     * Gets a methods description.
     * This is an example of a description. In PSR-5 it follows the summary.
     * @param string[] $lines
     * @return string
     */
    protected function getDescription(array $lines)
    {
        $description = '';
        $summaryFound = false;
        $summaryPassed = false;
        foreach ($lines as $line) {
            if ($line && !$summaryPassed) {
                $summaryFound = true;
                if (substr(trim($line), -1) == '.') {
                    $summaryPassed = true;
                }
                continue;
            }
            if (!$line && $summaryFound && !$summaryPassed) {
                $summaryPassed = true;
                continue;
            }
            if ($line && $line[0] == '@') {
                break;
            }
            if ($line && $summaryPassed) {
                $description .= $line."\n";
            }
        }
        return trim($description);
    }

This can be reduced by moving some of the complex comparisons into seperate methods, eg:

    private function isEndOfSummary($line, $summaryFound, $summaryPassed)
    {
        return (!$line || substr(trim($line), -1) == '.') && $summaryFound && !$summaryPassed;
    }

    private function hasDescriptionEnded($line)
    {
        return $line && $line[0] == '@';
    }

    /**
     * Gets a methods description.
     * This is an example of a description. In PSR-5 it follows the summary.
     * @param string[] $lines
     * @return string
     */
    protected function getDescription(array $lines)
    {
        $description = '';
        $summaryFound = false;
        $summaryPassed = false;

        foreach ($lines as $line) {
            if ($line && !$summaryPassed) {
                $summaryFound = true;
            }
            if ($this->isEndOfSummary($line, $summaryFound, $summaryPassed)) {
                $summaryPassed = true;
                continue;
            }
            if ($this->hasDescriptionEnded($line)) {
                break;
            }
            if ($line && $summaryPassed) {
                $description .= $line."\n";
            }
        }
        return trim($description);
    }

This takes the complexity below 10 and still passes the tests, though could be reduced further by moving the other if's out.

The question is, is it worth it, and do there methods belong here in the Documentation class?

Parse enpoint method parameters

Currently endpoint method parameters must be url friendly. These could be better parsed by the framework to match variables more closely. For example:

test?first-name=Daniel

should happily map to

getTestEndpoint($firstName)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.