Статьи

RESTful прокси удаленных объектов с ProxyManager

Эта статья была рецензирована Дежи Акала и Марко Пиветта . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Прокси-шаблонеще один крутой шаблон дизайна в разработке программного обеспечения. Прокси — это класс, работающий в качестве интерфейса с другим классом или веб-службой. Для простоты мы будем ссылаться на прокси-классы как на предметы в оставшейся части статьи. Прокси-сервер обычно реализует тот же интерфейс, что и субъект, поэтому похоже, что мы вызываем методы непосредственно для субъекта.

Хотя эта статья не о концепции шаблонов прокси, мы рассмотрим некоторые основы, только для начала.

Существует множество типов прокси, используемых для разных целей:

  1. Виртуальный прокси — для ленивой загрузки ресурсоемких объектов, не позволяя им занимать память до тех пор, пока они не понадобятся.

  2. Protection Proxy — для ограничения доступа к свойствам или методам объекта с помощью набора правил.

  3. Smart Reference — для добавления дополнительных поведений, когда метод вызывается по предмету — подходит для аспектно-ориентированного программирования.

  4. Удаленные объекты — для доступа к удаленным объектам, скрывающим тот факт, что они фактически находятся в отдельном адресном пространстве. Эта статья будет в основном посвящена прокси удаленных объектов.

Давайте начнем с базового примера виртуального прокси :

class MyObjectProxy { protected $subject; public function someMethod() { $this->init(); return $this->subject->someMethod(); } private function init() { if (null === $this->subject) { $this->subject = new Subject(); } } } 

В приведенном выше коде мы сохраняем ссылку на тему; каждый раз, когда метод вызывается для прокси, вызывается init() , проверяющий, был ли еще создан экземпляр субъекта, и если нет, то он будет создан.

Наконец, соответствующий метод по теме называется:

 // ... return $this->subject->someMethod(); // ... 

Это очень простой пример виртуального прокси-сервера владельца стоимости , который постоянно хранит ссылку на тему. Хотя предыдущий пример прекрасно работает, мы не создаем прокси в этом мире. Вместо этого мы будем делать то, что делал бы любой другой хороший разработчик: использовать хорошо известную, хорошо протестированную стороннюю библиотеку.

ProxyManager — это библиотека PHP для создания различных типов прокси через набор фабричных классов.

Марко Пиветта , автор ProxyManager, имеет исчерпывающую презентацию , которая представляет собой простое для понимания введение в шаблон Proxy. Я также рекомендую быстро взглянуть на официальную документацию ProxyManager, прежде чем мы начнем.

Резка в погоню

Этот пост посвящен одному из менее обсуждаемых типов прокси, известных как прокси удаленных объектов . Мы узнаем, что они из себя представляют и как они работают. Затем мы перейдем к созданию прокси удаленного объекта, который способен взаимодействовать с RESTful API .

Проще говоря, прокси удаленных объектов используются для взаимодействия с удаленными субъектами (используя HTTP в качестве транспортного механизма) и маскируют их как локальные объекты!

Всякий раз, когда метод вызывается для прокси-объекта, он пытается сопоставить имя вызываемого метода с его удаленным аналогом . Затем он кодирует и перенаправляет запрос удаленному методу. Все это происходит за считанные миллисекунды.

Мы создаем наши прокси удаленных объектов с помощью класса RemoteObjectFactory ProxyManager. Эта фабрика требует адаптера для создания прокси удаленного объекта, способного взаимодействовать с определенным удаленным субъектом. С другой стороны, адаптер отвечает за преобразование запроса таким образом, который может обрабатывать удаленный субъект.

Адаптеры зависят от внедренного протокола удаленным субъектом независимо от того, является ли тема XML-RPC, SOAP или даже RESTful JSON API, следует использовать соответствующий адаптер.

В следующем коде будет использоваться RemoteObjectFactory RemoteObjectFactory .

В настоящее время ProxyManager предоставляет три адаптера из коробки: XmlRpc , JsonRpc и Soap . Эти адаптеры используют реализацию интерфейса Zend\Server\Client в качестве транспортного механизма.

Чтобы увидеть примеры того, как это делается, официальная документация — лучшее место.

В следующем разделе мы создадим то, чего на данный момент не хватает: специальный адаптер, подходящий для API RESTful!

подготовка

Сначала мы создадим фиктивный JSON API в качестве нашего удаленного субъекта — используя Silex в качестве нашей платформы. Затем мы установим ProxyManager, чтобы создать для него прокси.

Поскольку ProxyManager не предоставляет адаптер для API RESTful, нам нужно создать свой собственный. Наш пользовательский адаптер будет использовать Guzzle в качестве HTTP-клиента.

Начнем с создания каталога, в котором будут храниться наши файлы:

 mkdir rest-proxy && cd rest-proxy 

Теперь мы устанавливаем Silex, ProxyManager и Guzzle (пока):

 composer require silex/silex ocramius/proxy-manager guzzlehttp/guzzle 

Структура каталогов

Мы реализуем тестовый API и код, связанный с прокси, в том же проекте, делая вид, что один локальный, а другой удаленный. Для удобства чтения мы помещаем эти два компонента в разные пространства имен.

Тем не менее, нам нужно изменить ключ autoload нашего файла composer.json :

Имя файла: composer.json

 { ... "autoload": { "psr-4": { "": "src/" } } } 

Затем мы запускаем composer dump-autoload .

В результате у нас может быть два разных пространства имен, сопоставленных с двумя разными каталогами в каталоге src/ . Назовем их RemoteProxy и API соответственно. Кроме того, мы создаем еще один каталог с именем web в корневом каталоге проекта, чтобы сохранить наш файл index.php . Структура каталогов теперь должна выглядеть так:

 remote-proxy ├── src │ ├── Api │ └── RemoteProxy └── web 

Создание API

У API будет три конечные точки для возврата списка книг, деталей книги и авторов книги.

Для этого мы создаем поставщика контроллеров Silex в src/Api/Controller как BookControllerProvider.php :

Чтобы увидеть, как работают поставщики контроллеров Silex, вы можете взглянуть на эту страницу .

Имя файла: src/Api/Controller/ApiControllerProvider.php

 <?php namespace Api\Controller; use Silex\Application; use Silex\Api\ControllerProviderInterface; use Symfony\Component\HttpFoundation\JsonResponse as Json; class ApiControllerProvider implements ControllerProviderInterface { public function connect(Application $app) { $controllers = $app['controllers_factory']; /** * Return a list of the books */ $controllers->get('books', function (Application $app) { return new Json([ 'books' => 'List of books...' ]); }); /** * Return book details by Id */ $controllers->get('books/{id}', function (Application $app, $id) { return new Json([ 'details' => 'Details of book with id ' . $id, ]); }); /** * Return the author(s) of the book */ $controllers->get('books/{id}/authors', function (Application $app, $id) { return new Json([ 'authors' => 'Authors of book with id ' . $id, ]); }); return $controllers; } } 

У нас есть три маршрута с контроллерами, каждый из которых возвращает экземпляр JsonResponse с фиктивным сообщением в качестве содержимого.

Создание приложения

Затем мы создаем наш файл src/app.php , в котором мы создаем приложение и монтируем наш контроллер контроллера:

Имя файла: src/app.php

 <?php use Silex\Application; use Api\Controller\ApiControllerProvider; $app = new Application(); $app->mount('/api/v1', new ApiControllerProvider()); return $app; 

Фронт-контроллер

Наконец, мы создаем фронт-контроллер, который будет точкой входа в наш API. Мы сохраняем этот файл как web/index.php со следующим содержимым:

Имя файла: web/index.php

 <?php require_once __DIR__.'/../vendor/autoload.php'; $app = require __DIR__ . '/../src/app.php'; $app->run(); 

В приведенном выше коде мы включаем автозагрузчик composer и наш файл app.php (который возвращает объект приложения Silex). Наконец, мы запускаем приложение, вызывая метод run() .

Чтобы проверить код, обратитесь к настройке сервера вашей среды (автоматически, если используется что-то вроде Homestead Improved ) или для краткости используйте встроенный веб-сервер PHP:

 php -S localhost:9000 -t web 

В результате последнего варианта API будет доступен по http://localhost:9000/api/v1 из браузера.

Создание прокси

Теперь, когда у нас все настроено на «удаленной» стороне, пришло время создать несколько прокси!

Прокси-адаптер

Адаптеры в ProxyManager расширяют абстрактный класс BaseAdapter , который реализует AdapterInterface .

Все адаптеры принимают объект клиента HTTP (через своего конструктора) для взаимодействия с удаленным субъектом вместе с массивом , который содержит информацию о сопоставлении метода с конечной точкой . Используя этот массив, адаптер узнает, какой удаленной конечной точке он должен переслать запрос.

В нашем случае массив выглядит так:

 <?php // ... $array = [ 'getBooks' => ['path' => 'books', 'method' => 'get'], 'getBook' => ['path' => 'books/:id', 'method' => 'get'], 'getAuthors' => ['path' => 'books/:id/authors', 'method' => 'get'], ]; // ... 

Каждый ключ является именем метода (в прокси-объекте), а значение (которое является массивом) имеет удаленный путь с методом для доступа к нему. Мы увидим, как этот массив используется позже.

Для создания нашего адаптера, в отличие от других адаптеров, мы не будем расширять абстрактный класс BaseAdapter . Вместо этого мы создадим его с нуля. Зачем это делать? Потому что абстрактный класс заставляет нас использовать реализацию Zend\Server\Client качестве нашего HTTP-клиента, в то время как мы хотим использовать Guzzle.

Адаптеры (реализующие AdapterInterface ) должны реализовать call() , который вызывается прокси при каждом вызове метода. Этот метод имеет три аргумента: сам проксируемый класс, имя вызываемого метода и аргументы вызываемого метода.

Вот код нашего адаптера RESTful:

Имя файла: src/RemoteProxy/Adapter/RestAdapter.php

 <?php namespace RemoteProxy\Adapter; use GuzzleHttp\ClientInterface; use ProxyManager\Factory\RemoteObject\AdapterInterface; class RestAdapter implements AdapterInterface { /** * Adapter client * * @var GuzzleHttp\ClientInterface */ protected $client; /** * Mapping information * * @var array */ protected $map; /** * Constructor * * @param Client $client * @param array $map Map of service names to their aliases */ public function __construct(ClientInterface $client, $map = []) { $this->client = $client; $this->map = $map; } /** * {@inheritDoc} */ public function call($wrappedClass, $method, array $parameters = []) { if (!isset($this->map[$method])) { throw new \RuntimeException('No endpoint has been mapped to this method.'); } $endpoint = $this->map[$method]; $path = $this->compilePath($endpoint['path'], $parameters); $response = $this->client->request($endpoint['method'], $path); return (string) $response->getBody(); } /** * Compile URL with its values * * @param string $path * @param array $parameters * * @return string */ protected function compilePath($path, $parameters) { return preg_replace_callback('|:\w+|', function ($matches) use (&$parameters) { return array_shift($parameters); }, $path); } } 

В предыдущем коде сначала мы устанавливаем свойства $map и $client в конструкторе класса. Внутри метода call() мы проверяем, существует ли имя вызываемого метода в свойстве $map ; если это так, мы вызываем compilePath() , помещая аргументы вызываемого метода в соответствующие заполнители в URL.

Например, если мы вызываем getBook(12) , URL-адрес может быть примерно таким http://localhost:9000/api/v1/book/12 .

Наконец, мы отправляем запрос в соответствии с указанным HTTP-глаголом — используя метод request() нашего клиента Guzzle:

 // ... $response = $this->client->request($endpoint['method'], $path); // ... 

Создание прокси-объекта

Теперь в любое время, когда нам нужно создать прокси удаленного объекта, мы создаем экземпляр ProxyManager\Factory\RemoteObjectFactory , передавая ему экземпляр нашего пользовательского адаптера и массив сопоставления .

Эту процедуру следует повторять везде, где нам нужно использовать прокси.

Пример использования:

 <?php // ... use ProxyManager\Factory\RemoteObjectFactory; use RemoteProxy\Adapter\RestAdapter; $base_uri = 'http://localhost'; // Creating the factory $factory = new RemoteObjectFactory( // Passing our custom adapter in new RestAdapter( // Passing our HTTP client to the adapter new \GuzzleHttp\Client([ 'base_uri' => $base_uri, ]), // Along with the arraym which contain the method-to-endpoint mapping $array = [ 'getBooks' => ['path' => 'books', 'method' => 'get'], 'getBook' => ['path' => 'books/:id', 'method' => 'get'], 'getAuthors' => ['path' => 'books/:id/authors', 'method' => 'get'], ]; ) ); $factory->createProxy('interfaceName'); // ... 

Обратите внимание, что базовый URI, который мы передаем в Guzzle во время создания экземпляра, является базовым URI нашего API.

Теперь мы создаем наш прокси-объект, вызывая createProxy() на фабрике. Этот метод требует интерфейса, методы которого реализуются удаленным субъектом. Тем не менее, нам также нужно иметь интерфейс. Давайте поместим этот файл в пространство имен RemoteProxy :

Файл: src/RemoteProxy/LibraryInterface.php

 <?php namespace RemoteProxy; interface LibraryInterface { /** * Return the books */ public function getBooks(); /** * Return book's details * * @param int $id * @return mixed */ public function getBook($id); /** * Return author of a book * * @param int $id * @return mixed */ public function getAuthors($id); } 

Теперь, когда у нас есть интерфейс, мы можем создать прокси:

 <?php // ... $library = $factory->createProxy(RemoteProxy\LibraryInterface::class); // calling the methods: var_dump($library->getBooks()); var_dump($library->getAuthors(1)); // ... 

Хранение картографической информации с интерфейсом

Хотя мы можем подготовить массив сопоставления во время создания экземпляра адаптера, есть более чистый способ сделать это. Мы определяем конечные точки с каждым методом в интерфейсе, используя пользовательскую аннотацию .

Что-то вроде:

 <?php interface LibraryInterface { /** * Return all books * @Endpoint(path="/books", method="get") */ public function getBooks(); // ... } 

Была использована пользовательская аннотация с именем Endpoint , указывающая путь и метод для доступа к ней.

Создание пользовательской аннотации — очень простая задача. Каждая аннотация представляет собой класс с тем же именем, параметры аннотации которого являются его общедоступными свойствами. Мы анализируем эту информацию с помощью анализатора аннотаций в массив, который можно отправить нашему адаптеру.

Для создания и анализа аннотации мы используем библиотеку Doctrine Annotations .

 composer require doctrine/annotations 

Каждый класс аннотаций должен быть аннотирован @Annotation , чтобы анализатор аннотаций Doctrine знал, что это допустимый класс аннотаций.

Давайте поместим класс аннотации в src/RemoteProxy/Annotation , чтобы иметь его в пространстве имен Annotation :

Файл: src/RemoteProxy/Annotation/Endpoint.php

 <?php namespace RemoteProxy\Annotation; /** * @Annotation */ class Endpoint { public $path; public $method; public function __construct($parameters) { $this->path = $parameters['path']; $this->method = isset($parameters['method']) ? $parameters['method'] : 'get'; } } 

В приведенном выше коде мы создаем свойство для каждого параметра аннотации.

В нашем случае это $path и $method . В конструкторе мы получаем параметры аннотации в виде ассоциативного массива ( $parameters ). Наконец, мы присваиваем элементы $parameters их соответствующим свойствам в классе.

Теперь мы обновляем интерфейс:

Имя файла: src/RemoteProxy/LibraryInterface.php

 <?php namespace RemoteProxy; use RemoteProxy\Annotation\Endpoint; interface LibraryInterface { /** * Return all books * * @Endpoint(path="books") */ public function getBooks(); /** * Return book's details * * * @Endpoint(path="books/:id") * @param int $id * @return mixed */ public function getBook($id); /** * Return author of a book * * @Endpoint(path="books/:id/authors") * @param int $id * @return mixed */ public function getAuthors($id); } 

Пока у нас есть аннотация и интерфейс, но мы должны проанализировать и передать их нашему адаптеру, который ожидает массив отображаемой информации.

Для этого мы создаем вспомогательный класс, который принимает интерфейс, извлекает информацию с помощью PHP Reflection API и анализатора аннотаций Doctrine и возвращает результат в виде массива. Затем мы можем передать этот массив нашему адаптеру — точно так же, как массив, который мы жестко закодировали в предыдущем примере.

Назовем класс UriResolver :

Имя файла: src/RemoteProxy/UriResolver.php

 <?php namespace RemoteProxy; use Doctrine\Common\Annotations\AnnotationReader; use RemoteProxy\Annotation\Endpoint; class UriResolver { /** * Annotation reader instance * * @var Doctrine\Common\Annotation\AnnotationReader */ protected $annotationReader; /** * Instantiate the URI resolver */ public function __construct() { $this->annotationReader = new AnnotationReader(); } /** * Return an array of mapping information * * @param string $interface * * @return array */ public function getMappings($interface) { $mappings = []; $methods = (new \ReflectionClass($interface))->getMethods(); foreach ($methods as $method) { $annotations = $this->annotationReader->getMethodAnnotations($method); foreach ($annotations as $annotation) { if ($annotation instanceof Endpoint) { $mappings[$method->name] = ['path' => $annotation->path, 'method' => $annotation->method]; } } } return $mappings; } } 

В приведенном выше коде сначала мы создаем экземпляр Doctrine\Common\Annotation\AnnotationReader и сохраняем его в свойстве $annotationReader .

В getMappings() мы получаем все методы интерфейса, используя API отражения. Затем мы перебираем их для анализа DocBlocks. На каждой итерации мы проверяем, есть ли у метода аннотация конечной точки. Если это так, мы добавляем новый элемент в массив $mappings с именем метода в качестве ключа и path и method качестве значений.

Вот как мы используем этот новый класс:

Пример использования:

 <?php // ... use ProxyManager\Factory\RemoteObjectFactory; use RemoteProxy\Adapter\RestAdapter; $base_uri = 'http://localhost'; AnnotationRegistry::registerLoader('class_exists'); $factory = new RemoteObjectFactory( new RestAdapter( new \GuzzleHttp\Client([ 'base_uri' => $base_uri, ]), // Array (new UriResolver())->getMappings(RemoteProxy\LibraryInterface::class) ) ); // ... 

Вы можете быть удивлены вызовом AnnotationRegistry::registerLoader('class_exists') . Поскольку аннотации Doctrine не загружаются определенными автозагрузчиками PHP, нам нужно использовать встроенный механизм автоматической загрузки Doctrine:

 <?php AnnotationRegistry::registerLoader('class_exists'); 

Создание особой фабрики

Каждый раз, когда мы хотим создать прокси удаленного объекта в нашем коде, нам нужно повторять описанные выше шаги, что, конечно, неудобно. Кроме того, это приводит к множеству экземпляров на наших контроллерах. В качестве решения мы абстрагируем вышеописанную процедуру в удобный фабричный класс.

Мы RestProxyFactory это RestProxyFactory src/RemoteProxy :

Имя файла : src/RemoteProxy/RestProxyFactory.php

 <?php namespace RemoteProxy; use Doctrine\Common\Annotations\AnnotationRegistry; use RemoteProxy\Adapter\RestAdapter; class RestProxyFactory { public static function create($interface, $base_uri) { // Registering a silent autoloader for the annotation AnnotationRegistry::registerLoader('class_exists'); $factory = new \ProxyManager\Factory\RemoteObjectFactory( new RestAdapter( new \GuzzleHttp\Client([ 'base_uri' => rtrim($base_uri, '/') . '/', ]), (new UriResolver())->getMappings($interface) ) ); return $factory->createProxy($interface); } } 

В вышеприведенном классе мы принимаем интерфейс и базовый URL API (нам это нужно для создания экземпляра Guzzle). Затем мы помещаем туда все предыдущие шаги инстанцирования.

использование

Теперь мы можем легко создать REST-дружественный прокси удаленного объекта в любое время, используя наш специальный класс фабрики.

Чтобы протестировать прокси, давайте определим маршрут в нашем файле src/app.php :

 <?php // src/app.php // ... $app->get('test-proxy', function (Application $app) { $proxy = new RestProxyFactory(LibraryInterface::class, 'localhost:9000/api/v1'); return new JsonResponse([ 'books' => $proxy->getBooks(), ]); }); // ... 

В приведенном выше коде getBooks() $proxy сделает HTTP-запрос к http://localhost:9000/api/v1/books . В результате мы должны получить следующий ответ:

 { resp: "{"books":"List of books..."}" } 

Это все, что нужно сделать. Я надеюсь, вам понравилась эта статья. Полный код доступен на Github, если вы хотите попробовать его самостоятельно.

Если вы просто хотите использовать его в своем проекте, вы можете установить его с помощью composer:

 composer require lavary/rest-remote-proxy 

Завершение

Мы узнали, как работают удаленные прокси и как они взаимодействуют с разными удаленными субъектами с разными протоколами. Мы также создали адаптер, подходящий для RESTful API.

Проксиируя удаленные объекты, мы можем использовать их, даже не зная, что они находятся в другом адресном пространстве. Мы просто просим, ​​и прокси делает звонки. Это так просто.

Как вы используете прокси? Как вы думаете, этот подход может пригодиться в некоторых ваших собственных проектах?