A simple facade for managing the relationship between your model and API.

Related tags

API grape-entity
Overview

Coverage Status php-grape

Table of Contents

PhpGrape\Entity

Introduction

Heavily inspired by ruby-grape/grape-entity.

This package adds Entity support to API frameworks. PhpGrape's Entity is an API focused facade that sits on top of an object model.

Installation

  composer require php-grape/grape-entity

Example

class StatusEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::formatWith('iso_timestamp', function($dt) {
      return $dt->format(DateTime::ISO8601);
    });

    self::expose('user_name');
    self::expose('text', [
      'documentation' => ['type' => 'String', 'desc' => 'Status update text'
    ]);
    self::expose('ip', ['if' => ['type' => 'full']]);
    self::expose('user_type', 'user_id', ['if' => function($status, $options) {
      return $status->user->isPublic();
    }]);
    self::expose('location', ['merge' => true]);
    self::expose('contact_info', function() {
      self::expose('phone');
      self::expose('address', ['merge' => true, 'using' => AddressEntity::class]);
    });
    self::expose('digest', function($status, $options) {
      return md5($status->text);
    });
    self::expose('replies', ['as' => 'responses', 'using' => StatusEntity::class]);
    self::expose('last_reply', ['using' => StatusEntity::class], function($status, $options) {
      return $status->replies->last;
    });

    self::withOptions(['format_with' => 'iso_timestamp'], function() {
      self::expose('created_at');
      self::expose('updated_at');
    });
  }
}

class StatusDetailedEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::extends(StatusEntity::class);

    self::expose('internal_id');
  }
}

Reusable Responses with Entities

Entities are a reusable means for converting PhP objects to API responses. Entities can be used to conditionally include fields, nest other entities, and build ever larger responses, using inheritance.

Defining Entities

Entities inherit from PhpGrape\Entity and use PhpGrape\EntityTrait. Exposures can use runtime options to determine which fields should be visible, these options are available to 'if', and 'func'.

PhP doesn't support multiple inheritance, so if you need your entities to inherits from multiple class use the extends method

Example:

class AEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::extends(BEntity::class, CEntity::class, DEntity::class);
  
    ...
  }
}

Basic Exposure

Define a list of fields that will always be exposed.

self::expose('user_name', 'ip');

The field lookup takes several steps

  • first try entity-instance->exposure
  • next try entity-instance->exposure()
  • next try object->exposure (magic __get method included)
  • next try object->exposure() (magic __call method included)
  • last raise a MissingPropertyException

NOTE: protected and private properties/methods are exposed by default. You can change this behavior by setting one or all of these static properties to true:

use PhpGrape\Reflection;

Reflection::$disableProtectedProps = true;
Reflection::$disablePrivateProps = true;
Reflection::$disableProtectedMethods = true;
Reflection::$disablePrivateMethods = true;
{ code: 418, message: "I'm a teapot" }">
class StatusEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('code');
    self::expose('message');
  }
}

$representation = StatusEntity::represent(['code' => 418, 'message' => "I'm a teapot"]);
json_encode($representation); // => { code: 418, message: "I'm a teapot" }

Exposing with a Presenter

Don't derive your model classes from Grape::Entity, expose them using a presenter.

self::expose('replies', ['as' => 'responses', 'using' => StatusEntity::class]

Conditional Exposure

Use 'if' to expose fields conditionally.

self::expose('ip', ['if' => ['type' => 'full']]);

// Exposed if the function evaluates to true
self::expose('ip', ['if' => function($instance, $options) {
  return isset($options['type']) && $options['type'] === 'full';
}]);

// Exposed if 'type' is set in the options array and is truthy
self::expose('ip', ['if' => 'type']);

// Exposed if $options['type'] is set and equals 'full'
self::expose('ip', ['if' => ['type' => 'full']]);

Safe Exposure

Don't raise an exception and expose as null, even if the field cannot be evaluated.

self::('expose', 'ip', ['safe' => true]);

Nested Exposure

Supply a function to define an array using nested exposures.

self::expose('contact_info', function() {
  self::expose('phone');
  self::expose('address', ['using' => Addressentity::class]);
});

You can also conditionally expose attributes in nested exposures:\

self::expose('contact_info', function() {
  self::expose('phone')
  self::expose('address', ['using' => AddressEntity::class]);
  self::expose('email', ['if' => ['type' => 'full']);
});

Collection Exposure

Use self::root(plural, singular = null) to expose an object or a collection of objects with a root key.

self::root('users', 'user');
self::expose('id', 'name');

By default every object of a collection is wrapped into an instance of your Entity class. You can override this behavior and wrap the whole collection into one instance of your Entity class.

As example:

// `collection_name` is optional and defaults to `items`
self::presentCollection(true, 'collection_name');
self::expose('collection_name', ['using' => ItemEntity::class]);

Merge Fields

Use 'merge' option to merge fields into the array or into the root:

self::expose('contact_info', function() {
  self::expose('phone');
  self::expose('address', ['merge' => true, 'using' => AddressEntity::class]);
});

self::expose('status', ['merge' => true]);

This will return something like:

{ contact_info: { phone: "88002000700", city: 'City 17', address_line: 'Block C' }, text: 'HL3', likes: 19 }

It also works with collections:

self::expose('profiles', function() {
  self::expose('users', ['merge' => true, 'using' => UserEntity::class]);
  self::expose('admins', ['merge' => true, 'using' => AdminEntity::class]);
});

Provide closure to solve collisions:

self::expose('status', 'merge' => function($key, $oldVal, $newVal) { 
  // You don't need to check if $oldVal is set here
  return $oldVal && $newVal ? $oldVal + $newVal : null;
});

Runtime Exposure

Use a closure to evaluate exposure at runtime. The supplied function will be called with two parameters: the represented object and runtime options.

NOTE: A closure supplied with no parameters will be evaluated as a nested exposure (see above).

self::expose('digest', function($status, $options) {
  return md5($status->txt);
});

You can also use the 'func' option, which is similar. Only difference is, this option will also accept a string (representing the name of a function), which can be convenient sometimes.

// equivalent
function getDigest($status, $options) {
  return md5($status->txt);
}

...

self::expose('digest', ['func' => 'getDigest']);

You can also define a method or a property on the entity and it will try that before trying on the object the entity wraps.

class ExampleEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('attr_not_on_wrapped_object');
  }

  private function attr_not_on_wrapped_object()
  {
    return 42;
  }
}

You always have access to the presented instance (object) and the top-level entity options (options).

object->value} {$this->options['y']}" } }">
class ExampleEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('formatted_value');
  }
  
  private function formatted_value()
  {
    return "+ X {$this->object->value} {$this->options['y']}"
  }
}

Unexpose

To undefine an exposed field, use the unexpose method. Useful for modifying inherited entities.

class UserEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('name');
    self::expose('address1');
    self::expose('address2');
    self::expose('address_state');
    self::expose('address_city');
    self::expose('email');
    self::expose('phone');
  }
}

class MailingEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::extends(UserEntity::class);
  
    self::unexpose('email');
    self::unexpose('phone');
  }
}

Overriding exposures

If you want to add one more exposure for the field but don't want the first one to be fired (for instance, when using inheritance), you can use the override flag. For instance:

class UserEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('name');
  }
}

class EmployeeEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::extends(UserEntity::class);

    self::expose('name', ['as' => 'employe_name', 'override' => true]);
  }
}

User will return something like this { "name" : "John" } while Employee will present the same data as { "employee_name" : "John" } instead of { "name" : "John", "employee_name" : "John" }.

Returning only the fields you want

After exposing the desired attributes, you can choose which one you need when representing some object or collection by using the only: and except: options. See the example:

class UserEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('id');
    self::expose('name');
    self::expose('email');
  }
}

class ExampleEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('id');
    self::expose('title');
    self::expose('user', ['using' => UserEntity::class]);
  }
}

$data = ExampleEntity::represent($model, [
  'only' => ['title', ['user' => ['name', 'email']]]
]);
json_encode($data);

This will return something like this:

{
  title: 'grape-entity is awesome!',
  user: {
  name: 'John Doe',
  email: '[email protected]'
  }
}

Instead of returning all the exposed attributes.

The same result can be achieved with the following exposure:

$data = ExampleEntity::represent($model, 
  'except' => ['id', ['user' => 'id']]
);
json_encode($data);

Aliases

Expose under a different name with 'as'.

self::expose('replies', ['as' => 'responses', 'using' => StatusEntity::class]);

Format Before Exposing

Apply a formatter before exposing a value.

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::formatWith('iso_timestamp', function($dt) {
      return $dt->format(DateTime::ISO8601);
    });

    self::withOptions(['format_with' => 'iso_timestamp'], function() {
      self::expose('created_at');
      self::expose('updated_at');
    });
  }
}

Defining a reusable formatter between multiples entities:

Entity::formatWith('utc', function($dt) {
  return $dt->setTimezone(new DateTimeZone('UTC'));
});

class AnotherEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('created_at', ['format_with' => 'utc']);
  }
}

Expose Null

By default, exposures that contain null values will be represented in the resulting JSON.

As an example, an array with the following values:

[
  'name' => null,
  'age' => 100
]

will result in a JSON object that looks like:

{
  "name": null,
  "age": 100
}

There are also times when, rather than displaying an attribute with a null value, it is more desirable to not display the attribute at all. Using the array from above the desired JSON would look like:

{
  "age": 100
}

In order to turn on this behavior for an as-exposure basis, the option expose_null can be used. By default, expose_null is considered to be true, meaning that null values will be represented in JSON. If false is provided, then attributes with null values will be omitted from the resulting JSON completely.

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('name', ['expose_null' => false]);
    self::expose('age', ['expose_null' => false]);
  }
}

expose_null is per exposure, so you can suppress exposures from resulting in null or express null values on a per exposure basis as you need:

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('name', ['expose_null' => false]);
    // since expose_null is omitted null values will be rendered
    self::expose('age');
  }
}

It is also possible to use expose_null with withOptions if you want to add the configuration to multiple exposures at once.

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    // None of the exposures in the withOptions closure will render null values
    self::withOptions(['expose_null' => false], function() {
      self::expose('name');
      self::expose('age');
    });
  }
}

When using withOptions, it is possible to again override which exposures will render null by adding the option on a specific exposure.

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    // None of the exposures in the withOptions closure will render null values
    self::withOptions(['expose_null' => false], function() {
      self::expose('name');
      // null values would be rendered in the JSON
      self::expose('age', ['expose_null' => true]);
    });
  }
}

Default Value

This option can be used to provide a default value in case the return value is null or false or empty (string or array).

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('name', ['default' => '']);
    self::expose('age', ['default' => 60]);
  }
}

Documentation

Expose documentation with the field. Gets bubbled up when used with various API documentation systems.

self::expose('text', [
  'documentation' => ['type' => 'String', 'desc' => "Status update text."]
]);

Options

The option key 'collection' is always defined. The 'collection' key is boolean, and defined as true if the object presented is iterable, false otherwise.

Any additional options defined on the entity exposure are included as is. In the following example user is set to the value of current_user.

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('user', ['if' => function($instance, $options) {
      return isset($options['user']);
    }], function($instance, $options) {
      return $options['user'];
    });
  }
}
MyEntity::represent($s, ['using' => StatusEntity::class, 'user' => current_user()]);

Passing Additional Option To Nested Exposure

Sometimes you want to pass additional options or parameters to nested a exposure. For example, let's say that you need to expose an address for a contact info and it has two different formats: full and simple. You can pass an additional full_format option to specify which format to render.

// api/ContactEntity.php
self::expose('contact_info', function() {
  self::expose('phone');
  self::expose('address', function($instance, $options) {
  // use `array_merge` to extend options and then pass the new version of options to the nested entity
  $options = array_merge(['full_format' => $instance->needFullFormat()], $options);
    return AddressEntity::represent($instance->address, $options);
  });
  self::expose('email', ['if' => ['type' => 'full']]);
}

// api/AddressEntity.php
// the new option could be retrieved in options array for conditional exposure
self::expose('state', ['if' => 'full_format']);
self::expose('city', ['if' => 'full_format']);
self::expose('street', function($instance, $options) {
  // the new option could be retrieved in options hash for runtime exposure
  return $options['full_format'] ? $instance->full_street_name : $instance->simple_street_name;
});

Notice: In the above code, you should pay attention to Safe Exposure yourself. For example, $instance->address might be null and it is better to expose it as null directly.

Attribute Path Tracking

Sometimes, especially when there are nested attributes, you might want to know which attribute is being exposed. For example, some APIs allow users to provide a parameter to control which fields will be included in (or excluded from) the response.

PhpGrape\Entity can track the path of each attribute, which you can access during conditions checking or runtime exposure via $options['attr_path'].

The attribute path is an array. The last item of this array is the name (alias) of current attribute. If the attribute is nested, the former items are names (aliases) of its ancestor attributes.

Example:

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::expose('user');  // path is ['user']
    self::expose('foo', ['as' => 'bar']);  // path is ['bar']
    self::expose('a', function() {
      self::expose('b', ['as' => 'xx'], function() {
      self::expose('c');  // path is ['a', 'xx', 'c']
      });
    });
  }
}

Using Entities

Example of API controller.

class ApiController
{
  use UserHelpers;  
  
  public function statuses()
  {
    $statuses = Status::all();
    $type = $this->user()->isAdmin() ? 'full' : 'default';
    $representation = StatusEntity::represent($statuses, ['type' => $type]);

    // PhpGrape\Entity implements JsonSerializable
    return response()->json($representation, 200);
  }
}

Key transformer

Most of the time backend languages use different naming conventions than frontend ones, so you often end up using helpers to convert your data keys case.

transformKeys helps you deal with that:

Entity::transformKeys(function($key) {
   return mb_strtoupper($key);
});

As a bonus, 2 case helpers are already included: camel & snake (handling 'UTF-8' unicode characters)

Example :

Entity::transformKeys('camel');

class MyEntity extends Entity
{
  use EntityTrait;

  private static function initialize()
  {
    self::root('current_user');
    self::expose('first_name', ['documentation' => ['desc' => 'foo']]);
  }
}

$representation = MyEntity::represent(['first_name' => 'John']);
json_encode($representation); // { currentUser: { firstName: 'John' } }
json_encode(MyEntity::documentation()); // { firstName: { desc: 'foo' } }

Adapters

For many reasons, you might need to access your properties / methods in a certain way. Or redefine the field lookup priority for instance (see Basic Exposure). PhpGrape\Entity lets you write your own adapter depending on your needs.

Adapter structure:

// Entity used is binded to $this 
//   so you have access to $this->object and $this->options
Entity::setPropValueAdapter('MyAdapter', [
  'condition' => function ($prop, $safe) {
    $model = 'MyProject\MyModel';
    return $this->object instanceof $model;
  },
  'getPropValue' => function ($prop, $safe, $cache) {
    $class = get_class($this->object);
  
    // you can use $cache to speed up future lookups
    if ($cache('get', $class, $prop)) return $this->object->{$prop};
  
    if ($this->object->hasAttribute($prop)) {
      $cache('set', $class, $prop);
      return $this->object->{$prop};
    }
  
    // Prop not found
    return $this->handleMissingProperty($prop, $safe);
  }
]);

To remove an adapter, simply set it to null:

Entity::setPropValueAdapter('MyAdapter', null);

NOTE: adapter names are unique. Using the same name will override previous adapter.

Laravel / Eloquent adapter

Eloquent relies massively on the magic __get method. Unfortunately, no Exception is thrown in case you access an undefined property, which is quite inconvenient in some situations. It doesn't help either with options like safe or expose_null.

To fix this, and to enjoy all the great PhpGrape\Entity features, an Eloquent adapter's been included. You'll still be able to use magic attributes, mutated attributes and access relations in your exposures. No more typo allowed though!

Testing with Entities

Test API request/response as usual.

Contributing

See CONTRIBUTING.md.

License

MIT License. See LICENSE for details.

You might also like...
Laravel api tool kit is a set of tools that will help you to build a fast and well-organized API using laravel best practices.
Laravel api tool kit is a set of tools that will help you to build a fast and well-organized API using laravel best practices.

Laravel API tool kit and best API practices Laravel api tool kit is a set of tools that will help you to build a fast and well-organized API using lar

Chargebee API PHP Client (for API version 2 and Product Catalog version 2.0)

chargebee-php-sdk Overview This package provides an API client for Chargebee subscription management services. It connects to Chargebee REST APIs for

It helps to provide API support to projects. It is Simple, Safe and Fast.

apiservice It helps to provide API support to projects. It is Simple, Safe and Fast. Setup composer create-project mind/apiservice or After downloadin

📒📚Generate beautiful interactive documentation and Open-API 3.0 spec file from your existing Laravel app.
Raidbots API wrapper which incorporates existing reports and static data into your project.

Raidbots API Raidbots API wrapper which incorporates existing reports and static data into your project. Usage use Logiek\Raidbots\Client; $client =

IMAGON is an image optimization and compression API Free, that helps improve your website performance.
IMAGON is an image optimization and compression API Free, that helps improve your website performance.

IMAGON API Demo Image Optimization and Compression API by IMAGON IMAGON is an image optimization and compression API Free, that helps improve your web

微信支付 API v3 的 PHP Library,同时也支持 API v2

微信支付 WeChatPay OpenAPI SDK [A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP 概览 微信支付 APIv2&APIv3 的Guzzle HttpClient封装组合, APIv2已内置请求数据签名及XML转换器,应

API documentation API SCB EASY APP

SCB-API-EASY V3.0 API documentation SIAM COMMERCIAL BANK PUBLIC COMPANY LTD. API SCB Easy V3 endpoint = https://fasteasy.scbeasy.link 1.0. Get balance

Courier API adalah project API untuk mengetahui ongkos kirim Logistik-logistik pengiriman barang antar kota & International
Courier API adalah project API untuk mengetahui ongkos kirim Logistik-logistik pengiriman barang antar kota & International

Courier API Courier API adalah project API untuk mengetahui ongkos kirim Logistik-logistik pengiriman barang antar kota (dalam negeri) & International

Comments
  • Exposing a NULL value not working

    Exposing a NULL value not working

    Exposing a field value NULL result in an error

    {
      "message": "method_exists(): Argument #1 ($object_or_class) must be of type object|string, null given",
      "exception": "TypeError",
      "file": "/var/www/html/vendor/php-grape/grape-entity/src/Entity.php",
      "line": 195,
    

    Here my code of my Entity. The field 'email_verified_at' is a timestamps, cast as Datetime in model.

        private static function initialize(): void
        {
            self::formatWith('iso_timestamp', function ($dt) {
                return $dt->format(DateTime::ISO8601);
            });
    
            self::expose('name');
    
            self::expose('email');
    
            self::withOptions(['format_with' => 'iso_timestamp'], function () {
                self::expose('email_verified_at');
                self::expose('created_at');
                self::expose('updated_at');
            });
        }
    
    opened by adrienpntl 3
Releases(v2.0.0)
Owner
null
Simple and effective multi-format Web API Server to host your PHP API as Pragmatic REST and / or RESTful API

Luracast Restler ![Gitter](https://badges.gitter.im/Join Chat.svg) Version 3.0 Release Candidate 5 Restler is a simple and effective multi-format Web

Luracast 1.4k Dec 14, 2022
Monorepo of the PoP project, including: a server-side component model in PHP, a GraphQL server, a GraphQL API plugin for WordPress, and a website builder

PoP PoP is a monorepo containing several projects. The GraphQL API for WordPress plugin GraphQL API for WordPress is a forward-looking and powerful Gr

Leonardo Losoviz 265 Jan 7, 2023
Monorepo of the PoP project, including: a server-side component model in PHP, a GraphQL server, a GraphQL API plugin for WordPress, and a website builder

PoP PoP is a monorepo containing several projects. The GraphQL API for WordPress plugin GraphQL API for WordPress is a forward-looking and powerful Gr

Leonardo Losoviz 265 Jan 7, 2023
This API provides functionality for creating and maintaining users to control a simple To-Do-List application. The following shows the API structure for users and tasks resources.

PHP API TO-DO-LIST v.2.0 This API aims to present a brief to consume a API resources, mainly for students in the early years of Computer Science cours

Edson M. de Souza 6 Oct 13, 2022
LaraBooks API - Simple API for iOS SwiftUI app tests.

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

Konrad Podrygalski 1 Nov 13, 2021
Simple PHP API client for tube-hosting.com rest API

Tube-Hosting API PHP client Explanation This PHP library is a simple api wrapper/client for the tube-hosting.com api. It is based on the provided docu

null 4 Sep 12, 2022
The 1Password Connect PHP SDK provides your PHP applications access to the 1Password Connect API hosted on your infrastructure and leverage the power of 1Password Secrets Automation

1Password Connect PHP SDK The 1Password Connect PHP SDK provides your PHP applications access to the 1Password Connect API hosted on your infrastructu

Michelangelo van Dam 12 Dec 26, 2022
A simple way of authenticating your RESTful APIs with API keys using Laravel

ApiGuard This package is no longer maintained This package is no longer maintained as Laravel already has a similar feature built-in since Laravel 5.8

Chris Bautista 691 Nov 29, 2022
A simple PHP project to make API requests on your cPanel installation

A simple PHP project to make API requests on your cPanel installation. This allows you to call modules inside the installation and interact with them to add, show or list data such as domains, e-mail accounts, databases and so on.

Elias Häußler 0 Sep 15, 2022
This API aims to present a brief to consume a API resources, mainly for students in the early years of Computer Science courses and the like.

Simple PHP API v.1.0 This API aims to present a brief to consume a API resources, mainly for students in the early years of Computer Science courses a

Edson M. de Souza 14 Nov 18, 2021