Нынче модно делать API и многие из нас уже реализовывали какие-то API на PHP. Одна из задач REST API — отдавать наборы данных, чтобы их в конечном итоге отобразить в табличном виде. Для этого, помимо прочего, приходится решать такие задачи:

  • провалидировать запрос,
  • отфильтровать данные,
  • отсортировать данные,
  • запрашивать и отдавать не все колонки, а только некоторые,
  • реализовать пагинацию.

Не знаю как вы, но я вижу, что часто это делается велосипедными решениями. Задачи с виду не сложные, но чтобы их решить качественно, приходится потратить немало времени на разработку, документацию и разъяснения коллегам, как работает ваше изобретение. Я расскажу о том, как можно реализовать эти задачи весьма технологично с помощью OData.

image

Я понимаю, что многим, кто дружит с LAMP, чужды веяния вражеских фронтов всевозможных Microsoft и Windows. Но давайте сперва посмотрим, что такое OData.

Что такое OData


Как написано на официальном сайте,
OData — the best way to REST

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

Примечательно то, что этот стандарт продвигает не только Microsoft. Стандарт одобрен организацией OASIS. Стандарт повсеместно используется в продуктах Microsoft и во многих больших системах других компаний.

Чем полезен OData


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

Кроме этого, у вас появляется возможность сэкономить время на разработке фронтэнда. Для этого можно взять готовые библиотеки, которые умеют работать с OData. Например, Kendo UI или бесплатный OpenUI5 от SAP.

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

Но почему тогда OData не используется в проектах на PHP


Или почти не используется. Действительно, я старательно погуглил на тему PHP+OData, но нашел не так уж и много. А то, что я нашел, заставило меня поломать голову — как добиться, чтобы это заработало.

С точки зрения реализации можно выделить две важные составляющие компоненты — клиент (запрашивает данные) и сервер (отдает данные). Далее речь пойдет о реализации серверной части.

Что удалось найти. Есть отличный проект от Microsoft, с открытыми исходниками, он выложен прямо на гитхабе: odataphpprod. По сути, это фреймворк для создания серверной части на PHP, которая работает по протоколу OData.

Я попытался его использовать и сразу столкнулся с двумя препятствиями. Во-первых, проект не заработал на Linux, хотя в readme проекта сказано, что поддерживаются обе системы — Windows и Linux. Чтобы проект заработал на Linux, пришлось в исходниках править места, где подключаются файлики — там путаница в путях и регистрах. Во вторых, чтобы реализовать простую отдачу списка, пришлось почитать не самую короткую инструкцию.

Кроме того, за последние 3 года в этом проекте не было ни одного коммита.

Я продолжил поиск, но не нашел других достойных реализаций, кроме форка вышеупомянутого проекта. Форк называется POData. В readme этого проекта сказано, что PHP разработчики — бедняжки, потому что у них нет хорошего инструментария для работы с OData, но библиотека берется нивелировать это недоразумение и привнести преимущества OData в массы. Я попробовал использовать этот форк, с ним дело пошло гораздо лучше. Проект заработал на Linux без дополнительных усилий. Там же есть и пошаговая простая дока, как использовать проект вместе с Zend Framework.

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

В общем, я вижу две причины, почему PHP разработчики до сих пор не дружат с OData:

  • порог вхождения — много кода дописывать, документация не очень простая, мало пошаговых примеров,
  • сыроватые инструменты.

Моя попытка снизить порог вхождения и пример OData-сервиса


Как я уже сообщил выше, много кода, который приходится реализовывать для работы с POData, вполне многоразовый и с большой вероятностью будет переноситься из проекта в проект без изменений. Поэтому я вынес этот код в отдельную библиотеку SimplePOData. Это реализация IQueryProvider (для реляционных SQL баз типа MySQL) и IService.

Далее — пример построения серверной части на чистом PHP без использования каких-либо фреймворков, кроме разве что самого POData.

Шаг 0. Устанавливаем необходимые библиотеки

Создайте каталог www/odata-example для первого проекта с OData. Создайте файл composer.json со следующим содержимым:

{
    "require": {
        "qeti/simple-podata": ">=0.9.1"
    }
}

Небольшое лирическое отступление. Проект POData/POData на гитхабе — по большому счету хороший рабочий проект. Но, судя по данным гитхаба, активная работа над ним закончилась два года назад. Чтобы использовать проект под свои нужды, я внес небольшие доработки, отправил пулл реквесты, но автор проекта не ответил до сих пор, даже когда я попытался связаться с ним через социальную сеть. Я надеюсь, что он выйдет на связь. Но пока что я прописал в SimplePOData свой форк Qeti/POData, куда закомитил свои изменения. Для удобства все добавил в Packagist. Если автор POData выйдет на связь и будет активно принимать изменения, то смысла в еще одном форке не будет и я переключусь на его форк.

Итак, чтобы установить необходимые пакеты, выполняем из консоли:

composer install

Шаг 1. Работа с URL

Описание метаданных будет доступно по адресу http://localhost/odata-example/odata.svc/$metadata.
Значит, нам надо сделать, чтобы все запросы к нашему OData сервису (http://localhost/odata-example/odata.svc) шли на index.php. Для этого в корне проекта разместите файл .htaccess со следующим содержимым:

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteRule    (\.svc.*) index.php
</IfModule>

Всю остальную работу по разбору URL возьмет на себя POData.

Шаг 2. Реализация IHTTPRequest

Мы должны реализовать интерфейс IHTTPRequest. Этот класс будет использовать POData для того, чтобы получить параметры URL. Реализация может различаться в зависимости от используемого фреймворка. Мы пойдем простым путем и будем передавать в конструктор $_GET. Создайте файл RequestAdapter.php со следующим содержимым:

<?php

use POData\Common\ODataConstants;
use POData\OperationContext\HTTPRequestMethod;
use POData\OperationContext\IHTTPRequest;

class RequestAdapter implements IHTTPRequest
{
    protected $request;

    public function __construct($request)
    {
        $this->request = $request;
    }

    /**
     * get the raw incoming url
     *
     * @return string RequestURI called by User with the value of QueryString
     */
    public function getRawUrl()
    {
        return $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . '/' . $_SERVER['REQUEST_URI'];
    }

    /**
     * get the specific request headers
     *
     * @param string $key The header name
     *
     * @return string|null value of the header, NULL if header is absent.
     */
    public function getRequestHeader($key)
    {
        if (isset($this->request[$key])) {
            return $headers = $this->request[$key];
        }
        return null;
    }

    /**
     * Returns the Query String Parameters (QSPs) as an array of KEY-VALUE pairs.  If a QSP appears twice
     * it will have two entries in this array
     *
     * @return array[]
     */
    public function getQueryParameters()
    {
        $data = [];
        if (is_array($this->request)) {
            foreach ($this->request as $key => $value) {
                $data[] = [$key => $value];
            }
        }
        return $data;
    }

    /**
     * Get the HTTP method/verb of the HTTP Request
     *
     * @return HTTPRequestMethod
     */
    public function getMethod()
    {
        return new HTTPRequestMethod('GET');
    }
}

Шаг 3. Реализация IOperationContext

Этот класс еще проще, он должен реализовать всего два метода — возвращать объекты запроса и ответа. Объект запроса — это экземпляр вышеописанного класса. Объект ответа — экземпляр OutgoingResponse, который уже реализован в POData так что с этим классом можно не разбираться на данном этапе. Создайте файл OperationContextAdapter.php:

<?php

use POData\OperationContext\IHTTPRequest;
use POData\OperationContext\IOperationContext;
use POData\OperationContext\Web\OutgoingResponse;

class OperationContextAdapter implements IOperationContext
{
    /**
     * @var RequestAdapter;
     */
    protected $request;

    protected $response;

    /**
     * @param yii\base\Request $request
     */
    public function __construct($request)
    {
        $this->request = new RequestAdapter($request);
        $this->response = new OutgoingResponse();
    }

    /**
     * Gets the Web request context for the request being sent.
     *
     * @return OutgoingResponse reference of OutgoingResponse object
     */
    public function outgoingResponse()
    {
        return $this->response;
    }

    /**
     * Gets the Web request context for the request being received.
     *
     * @return IHTTPRequest reference of IncomingRequest object
     */
    public function incomingRequest()
    {
        return $this->request;
    }
}

Шаг 4. Реализация IQueryProvider

Задача этого класса — выбрать данные из вашего источника. Насколько я понял из документации к фреймворку, разработчики предлагают реализовать в этом классе не только функциональность для извлечения данных, но мэппинг источника данных с названиями таблиц и колонок в базе. В принципе, в большинстве случаев названия таблиц будут явно соответствовать названию сервисов в URL-ах. Поэтому в SimplePOData реализованы необходимые методы, но наложено ограничение. Название сервиса в URL DocumentHasProduct будет преобразовано в название таблицы document_has_product. Если вас это не устраивает, то можете переопределить метод getTableName().

Все, что вам остается сделать — реализовать метод получения множества строк и метод получения одного значения из вашего источника данных. В нашем примере мы будем работать с PDO. Создайте файл QueryProvider.php:

<?php

use qeti\SimplePOData\BaseQueryProvider;

class QueryProvider extends BaseQueryProvider
{
    public function __construct(\PDO $db){
        parent::__construct($db);
    }

    /**
     * Get associated array with rows
     * @param string $sql SQL query
     * @param array $parameters Parameters for SQL query
     * @return mixed[]|null
     */
    protected function queryAll($sql, $parameters = null)
    {
        $statement = $this->db->prepare($sql);
        $statement->execute($parameters);
        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Get one value
     * @param string $sql SQL query
     * @param array $parameters Parameters for SQL query
     * @return mixed|null
     */
    protected function queryScalar($sql, $parameters = null)
    {
        $statement = $this->db->prepare($sql);
        $statement->execute($parameters);
        $data = $statement->fetchAll(PDO::FETCH_COLUMN);
        if ($data) {
            return $data[0];
        }
        return null;
    }

}

Шаг 5. Классы, описывающие данные

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

CREATE TABLE product (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  added_at TIMESTAMP DEFAULT NOW(),
  name VARCHAR(250),
  weight DECIMAL(10, 4),
  code VARCHAR(45)
);

Добавьте в нее тестовых данных
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (6,'2013-05-07 00:00:00','Kedi',2.9200,'Ked-25');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (9,'2009-08-05 00:00:00','Kedi',10.9100,'Ked-51');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (13,'2003-02-27 00:00:00','Kedi',11.7300,'Ked-17');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (29,'2014-12-19 00:00:00','Kedi',7.6100,'Ked-29');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (33,'2003-07-05 00:00:00','Kedi',11.8700,'Ked-99');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (36,'2015-09-15 00:00:00','Kedi',11.0000,'Ked-89');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (40,'2004-01-25 00:00:00','Kedi',14.8800,'Ked-83');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (47,'2006-04-23 00:00:00','Kedi',1.2100,'Ked-62');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (51,'2012-12-08 00:00:00','Kedi',12.4000,'Ked-86');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (54,'2010-06-09 00:00:00','Kedi',6.3800,'Ked-61');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (58,'2010-04-25 00:00:00','Kedi',8.8900,'Ked-74');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (106,'2004-04-11 00:00:00','Kedi',6.7100,'Ked-44');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (134,'2001-02-07 00:00:00','Kedi',2.3200,'Ked-29');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (153,'2002-01-13 00:00:00','Kedi',7.3300,'Ked-80');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (156,'2014-03-20 00:00:00','Kedi',10.9600,'Ked-30');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (165,'2003-07-11 00:00:00','Kedi',2.5300,'Ked-90');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (176,'2010-09-26 00:00:00','Kedi',7.0100,'Ked-38');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (182,'2007-05-07 00:00:00','Kedi',3.8900,'Ked-6');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (194,'2004-03-21 00:00:00','Kedi',3.1000,'Ked-20');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (205,'2000-06-02 00:00:00','Kedi',12.9500,'Ked-20');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (212,'2002-02-20 00:00:00','Kedi',2.5300,'Ked-62');
INSERT INTO `product` (`id`,`added_at`,`name`,`weight`,`code`) VALUES (220,'2000-10-19 00:00:00','Kedi',8.4000,'Ked-31');


И опишите класс для этой таблицы, создайте файл models/Product.php:

<?php

namespace models;

use qeti\SimplePOData\EntityTrait;

class Product {

    // This trait contains method for fields mapping (between database table and this class)
    use EntityTrait;

    public $id;
    public $added_at;
    public $name;
    public $weight;
    public $code;
}

Как видно, все, что есть в этом классе — перечисление полей таблицы product и подключение трейта EntityTrait, в котором реализован мэппинг названий свойств класса и полей базы. В этой реализации названия полей БД точно соответствует названию свойств класса. Те, кого это по каким-то причинам не устраивает, могут просто сделать другую реализацию статического метода fromRecord().

Шаг 6. Метаданные

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

Создайте файл models/MetadataProvider.php:

<?php

namespace models;

use POData\Providers\Metadata\Type\EdmPrimitiveType;
use POData\Providers\Metadata\SimpleMetadataProvider;

class MetadataProvider
{
    const MetaNamespace = "Data";

    /**
     * Description of service
     *
     * @return IMetadataProvider
     */
    public static function create()
    {
        $metadata = new SimpleMetadataProvider('Data', self::MetaNamespace);
        $metadata->addResourceSet('Products', self::createProductEntityType($metadata));
        return $metadata;
    }

    /**
     * Describtion of Products
     */
    private static function createProductEntityType(SimpleMetadataProvider $metadata)
    {
        $et = $metadata->addEntityType(new \ReflectionClass('\models\Product'), 'Products', self::MetaNamespace);

        $metadata->addKeyProperty($et, 'id', EdmPrimitiveType::INT32); 
        $metadata->addPrimitiveProperty($et, 'added_at', EdmPrimitiveType::DATETIME);
        $metadata->addPrimitiveProperty($et, 'name', EdmPrimitiveType::STRING);
        $metadata->addPrimitiveProperty($et, 'weight', EdmPrimitiveType::DECIMAL);
        $metadata->addPrimitiveProperty($et, 'code', EdmPrimitiveType::STRING);

        return $et;
    }

}

Тут мы описали, что у нас есть коллекция Products (http://localhost/odata-example/odata.svc/Products) и какие в ней есть поля.
addKeyProperty() определяет ключевое поле. Это поле используется для фильтрации, когда вы выбираете конкретную запись, запрашивая http://localhost/odata-example/odata.svc/Products(1). addPrimitiveProperty() определяет обычное поле.

Шаг 7. index.php

Ну вот и все. Остается создать index.php, в котором надо подключить созданные классы, создать соединение с базой и попросить POData обработать запрос.

<?php

use POData\OperationContext\ServiceHost;
use qeti\SimplePOData\DataService;

require(__DIR__ . '/vendor/autoload.php');

require(__DIR__ . '/OperationContextAdapter.php');
require(__DIR__ . '/RequestAdapter.php');
require(__DIR__ . '/QueryProvider.php');
require(__DIR__ . '/models/MetadataProvider.php');
require(__DIR__ . '/models/Product.php');

// DB Connection
$dsn = 'mysql:dbname=yourdbname;host=127.0.0.1';
$user = 'username';
$password = 'password;
$db = new \PDO($dsn, $user, $password);

// Realisation of QueryProvider
$db->queryProviderClassName = '\\QueryProvider';

// Controller
$op = new OperationContextAdapter($_GET);
$host = new ServiceHost($op);
$host->setServiceUri("/odata.svc/");
$service = new DataService($db, \models\MetadataProvider::create());
$service->setHost($host);
$service->handleRequest();
$odataResponse = $op->outgoingResponse();

// Headers for response
foreach ($odataResponse->getHeaders() as $headerName => $headerValue) {
    if (!is_null($headerValue)) {
        header($headerName . ': ' . $headerValue);
    }
}

// Body of response
echo $odataResponse->getStream();

Что получили в результе


В результате проделанного выше получаем сервис, который умеет обрабатывать такие запросы.

odata.svc
Возвращает список коллекций

<service xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app"xml:base="http://localhost:80/OData-base-example/odata.svc">
<workspace>
<atom:title>Default</atom:title>
<collection href="Products">
<atom:title>Products</atom:title>
</collection>
</workspace>
</service>

odata.svc/$metadata возвращает описание сущностей.

<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" Version="1.0">
<edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx"m:DataServiceVersion="1.0">
<Schema xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"xmlns="http://schemas.microsoft.com/ado/2007/05/edm" Namespace="Data">
<EntityType Name="Products">
<Key>
<PropertyRef Name="id"/>
</Key>
<Property Name="id" Type="Edm.Int32" Nullable="false"/>
<Property Name="added_at" Type="Edm.DateTime" Nullable="true"/>
<Property Name="name" Type="Edm.String" Nullable="true"/>
<Property Name="weight" Type="Edm.Decimal" Nullable="true"/>
<Property Name="code" Type="Edm.String" Nullable="true"/>
</EntityType>
<EntityContainer Name="Data" m:IsDefaultEntityContainer="true">
<EntitySet Name="Products" EntityType="Data.Products"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

odata.svc/Products возвращает все записи из коллекции Products. Если это большая коллекция, то так вызывать не стоит — лучше ограничивать выборку. Наример, если вызвать odata.svc/Products?&$format=json&$filter=id le 5&$orderby=id desc,
то произойдет следующее. Будут выбраны записи с id не более 5, данные отсортируются по id в обратном порядке. Результат будет отдан в формате json:

{"odata.metadata":"http://localhost:80/odata-example/odata.svc/$metadata#Products","value":
[{"id":5,"added_at":"2006-07-14T00:00:00","name":"Kon","weight":"14.1700","code":"Kon-59"},
{"id":4,"added_at":"2014-03-16T00:00:00","name":"Kon","weight":"2.4100","code":"Kon-89"},
{"id":3,"added_at":"2009-07-23T00:00:00","name":"Bicycle","weight":"4.3100","code":"Bic-18"},
{"id":2,"added_at":"2000-03-25T00:00:00","name":"Samokat","weight":"8.0200","code":"Sam-96"},
{"id":1,"added_at":"2006-10-22T00:00:00","name":"Kolyaska","weight":"10.1300","code":"Kol-97"}]}

Насчет $filter, тут стоит обратить отдельное внимание, что условия можно накручивать непростые, использовать скобки, операторы and, or и многое другое. При разборе условия выполняются необходимые проверки и исключается возможность внедрения SQL-инъекций.

Пример организации пагинации и выбора только указанных колонок: odata.svc/Products?$skip=10&$top=5&$format=json&$select=id,name&$inlinecount=allpages

{"odata.metadata":"http://localhost:80/odata-example/odata.svc/$metadata#Products", "odata.count":"1002", "value":
[{"id":11,"name":"Motoroller"},
{"id":12,"name":"Kolyaska"},
{"id":13,"name":"Kedi"},
{"id":14,"name":"Roliki"},
{"id":15,"name":"Doska"}]}

При указании $inlinecount=allpages вы получаете в ответе в поле odata.count количество записей в выборке, как если бы не применялся оператор LIMIT в SQL запросе.

odata.svc/Products(5)?$format=json
Возвращает данные о продукте с id=5
{
  "odata.metadata":"http://localhost:80/odata-example/odata.svc/$metadata#Products/@Element",
  "id":1, "added_at":"2006-10-22T00:00:00", "name":"Kolyaska", "weight":"10.1300", "code":"Kol-97"
}

odata.svc/Products/$count — количество записей.

Тем, кто не хочет делать копи-паст


Если вы заинтересовались использованием OData в своем проекте на PHP, хотите сделать примерчик, но не хотите копипастить, можете поступить еще проще. Вышеописанный пример есть на гитхабе — следуйте инструкциям из readme.

Бонус


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

image

Резюме


Что хорошего

  • Оказывается, рабочая реализация серверной части OData есть и на PHP.
  • Позволяет сэкономить немало сил при разработке фронтэнда.
  • Готовая реализация первичной валидации, фильтрации и сортировки данных, пагинация.
  • Защита от SQL инъекций.

Какие есть минусы

  • Пока что в POData есть поддержка только GET запросов.
  • На данный момент маловато примеров использования.

Лично я, взвесив плюсы и минусы, склоняюсь к тому, что технологию стоит использовать в своих проектах. Если в POData чего-то не хватает (например, INSERT, UPDATE, DELETE операций), то туда несложно добавить недостающее. Надеюсь, что и вам удастся извлечь для себя пользу из материала.

Мне хотелось бы получить побольше обратной связи. Будете ли работать с OData в своих проектах? Что думаете о вышесказанном?

Комментарии (31)


  1. Igogo2012
    28.09.2015 09:36
    -1

    Чем Вам не подошел какой либо PHP RESTful микрофреймворк (Slim, Lumen, Silex, etc.), или вообще можно взять Yii Framework 2 где из коробки есть возможность сгенерировать RESTFul контроллеры для любых моделек?

    ИМХО. Исходя из поста создалось впечатление что его название должны было выглядеть так — «PHP и OData: пересаживаемся с PHP RESTful-фреймворка на велосипед от Microsoft»


    1. mnv
      28.09.2015 10:45
      +3

      Я пользовался Yii2 RESTful. Это отличный инструмент. Но столкнулся с тем, что приходится делать вещи, которые можно было бы не делать. Например, понадобилась произвольная фильтрация. В OData можно сделать условие типа такого
      $filter=((weight ge 100 or weight le 10) and price le 15)
      И оно будет работать без какого-либо дополнительного программирования.


      1. Igogo2012
        28.09.2015 11:15
        +1

        Возможно я не совсем понял, но если эти фильтры задаются клиентом — ((weight ge 100 or weight le 10) and price le 15) то выходит как-то не кошерно, особенно если у нас будет get запрос.

        А если на сервере, то как бы в Yii2 это тоже решается в одну строку, добавлением простого where —

        $query->where( [ [ 'or', 'weight<100',  'weight>10' ], 'and', 'price>1' ] ) 
        


        1. mnv
          28.09.2015 11:41
          +3

          В случае с OData не надо ничего писать для извлечения данных, если действовать как описано в статье. Yii2 дает готовый инструмент только на уровне контроллеров, при этом надо самому делать валидацию, защиту от SQL инъекций. Например, так просто нельзя ведь написать, 'weight<100'. Надо как минимум проверить, что пришедший с клиента weight это колонка, а не что-то вроде '; drop table ... --'.
          POData дает готовый инструмент не только на уровне контроллеров, но и сам делает валидацию, помогает в извлечении данных. А если использовать стандартный грид с поддержкой OData (типа OpenUI5), то о том, как будет выглядеть GET запрос, можно даже не заботиться, грид сформирует запрос за вас.


          1. vintage
            28.09.2015 12:03
            +4

            1. Нет ничего страшного в weight === '; drop table… --', если вы правильно работаете с базой.
            2. Довольно опасно давать клиенту делать любые запросы к базе — он с лёгкостью может устроить вам DOS сделав запрос по непроиндексированным полям в большой таблице.


            1. mnv
              28.09.2015 12:33
              +1

              Насчет 1, дело в том, что валидировать надо не только значения, но и названия колонок, по которым выполняется фильтрация, если это название получено с клеинта.
              По поводу 2, замечание совершенно замечательное. Такие вещи можно обрабатывать в обертке над QueryProvider, делать дополнительную проверку для нужных таблиц, чтобы было такое-то условие. Это может быть не всегда просто в реализации, но делать это действительно стоит.


              1. vintage
                28.09.2015 12:51

                Названия колонок тоже нужно экранировать в SQL запросе.


                1. mnv
                  28.09.2015 14:32

                  Можно пример? Мне приходит на ум только сопоставление названий со схемой базы. Но в любом случае для этого надо сперва распарсить условие. Либо передавать условие от клиента сразу в виде дерева. Помимо этого и операторы тоже надо валидировать. На мой взгляд в итоге получается громоздко и это надо писать самому. Либо можно взять готовое и использовать OData как вариант.


                  1. vintage
                    28.09.2015 19:02

                    select from `from` where `where` = 123
                    


                    1. mnv
                      28.09.2015 19:17

                      а если с клиента пришло вместо where:
                      where`=1; drop table ... --?


                      1. vintage
                        28.09.2015 19:25

                        exception


                        1. mnv
                          28.09.2015 19:33

                          Ну вот, вы ожидаете, что пришел например id и делаете запрос
                          select * from `account` where `id` = 1;
                          а пришел не id, а ` = 1; select 1; --
                          Тогда вы сгенеририуете такой sql:
                          select * from `account` where `id` = 1; select 1; -- ` = 1
                          Он выполнится без exception. Вместо select 1 можно указать что угодно.
                          Поэтому стоит сравнивать названия колонок со схемой, да и условия >=, < и т.д. тоже валидировать.


                          1. zelenin
                            28.09.2015 19:44

                            а pdo-биндинг для чего?


                            1. mnv
                              28.09.2015 19:48

                              Для биндинга значений


                              1. zelenin
                                28.09.2015 19:51

                                то есть вы хотите сказать, что биндинг, встроенный в pdo (используемый в yii2), не защитит в данном случае от инъекции?


                          1. vintage
                            28.09.2015 20:58
                            +1

                            Вы экранирование забыли:

                            select * from `account` where `id\` = 1; select 1; -- ` = 1
                            



                            1. mnv
                              28.09.2015 21:28

                              С экранированием да, лучше. Но опять же, надо парсить самостоятельно запрос от клиента. А можно этого не делать, а взять готовый инструмент.


                              1. vintage
                                28.09.2015 21:48

                                odata.svc/Products?&$format=json&$filter=id le 5&$orderby=id desc,
                                Да ну нафиг такое парсить :-) Лучше такое:

                                /product.id<5.^id.json


          1. Igogo2012
            28.09.2015 12:04
            +1

            надо самому делать валидацию, защиту от SQL инъекций

            Не надо ничего делать самому, там же PDO, просто написать не weight<100, а передать параметром — weight<:weight и забиндить значения, передавая в тот же where дополнительный массив со значениями, а PDO сам все сделает.

            стандартный грид с поддержкой OData (типа OpenUI5)

            В yii есть же GridView
            Да и вообще для любого RESTFul фреймворка можно использовать любой RESTFul грид, которых много в сети.

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


            1. mnv
              28.09.2015 12:24

              'weight<:weight'

              Само слово weight, полученное с клиента надо тоже валидировать, я об этом, не о значении 100.
              пришлось дописать в разы больше

              Ну не в разы, но много писать самому пришлось, я об этом упомянул. Но надо учесть, что я как-то уже реализовывал универсальный механизм и видел, как коллеги делали универсальный механизм для фильтрации данных — там кода было куда больше. А если не делать универсальный механизм, то придется много писать кода под конкретные случаи, что будет путаться с бизнес-логикой, это труднее на мой взгляд поддерживать.
              GridView штука хорошая, но и в ней к сожалению есть ограничения. OData дает больше возможностей, меня это и привлекает. Не панацея, но инструмент на мой взгляд действительно достойный.


              1. vintage
                28.09.2015 13:01

                Как в OData конкурентно добавлять значения в коллекции?


                1. mnv
                  28.09.2015 14:24

                  В настоящий момент в POData добавление не реализовано, только GET. Если мне понадобится в процессе добавление, редактирование, удаление, буду реализовывать. Насчет конкурентно добавлять уточните, пожалуйста, что имеете ввиду — вариант обернуть запросы в транзакцию подходит?


                  1. vintage
                    28.09.2015 19:10

                    Что значит «если»? У вас ридонли апи, что ли? :-)
                    Конкурентно — это два пользователя, например, одновременно добавляют в коллекцию каждый своё значение. Как строятся их запросы, чтобы приславший запрос вторым не затёр изменения внесённые первым?


                    1. mnv
                      28.09.2015 19:25

                      В библиотеке реализовано чтение данных. Запись можно реализовать самому. В целях POData автор форка ставил реализацию INSERT, UPDATE, DELETE, но до сих пор это не сделано. В принципе, при реализации можно и учесть моменты с конкурентными запросами, хотя это не всегда надо. Скажите для примера, где это хорошо учтено?


                      1. vintage
                        28.09.2015 19:31
                        +1

                        В моём велосипеде передаётся диф изменений. В OData как-то не нашёл ничего подобного. Это вообще беда многих апи, а от «стандарта» ожидаешь, что детских болезней там не будет.


                        1. mnv
                          28.09.2015 19:35

                          А что, хороший вариант


  1. alan008
    28.09.2015 09:42
    -7

    пагинацию

    пейджинацию тогда уж. А лучше по-русски — постраничное представление.



  1. Razaz
    28.09.2015 20:42
    +2

    Мы то же рассматривали OData, благо серверный стек — WebApi, но решили отказаться по следующим причинам:
    1. Хранилища данных не всегда поддершивают одну и ту же модель данных. Например различаются свойства объектов, операции фильтрации, которые можно над ними совершать. Затягивать весь объем данных — просто нереально. Или например джоины данных в памяти.
    2. Запросы прекрасны до тех пор, пока вы контролируете клиентов этого апи. Если вы их не контролируетет, то выдавать такой широкий спектр фильтров — опасно, так как могут возникнуть очень жуткие головняки с обратной совместимостью.

    Из всего этого просто сделали Hypermedia Based Api по типу Collection+Json.


  1. hlogeon
    29.09.2015 10:18

    Нет, ну ладно Вам не угодил Yii2, ладно микрофреймворки. А какие преимущества у OData перед Apigility? Мне кажется, вы испытали НАМНОГО больше боли при попытке построения REST, чем должны были. А потом еще выяснилось, что POST/PUT/UPDATE и DELETE там вообще не реализованы.


    1. mnv
      29.09.2015 22:12

      Для чего мне понадобилась OData. У меня проект на Yii2. Проект представляет собой REST API. Для него мне понадобился инструмент для произвольных выборок из базы, чтобы внутренние клиенты компании могли вести разработку своих проектов не ожидая, пока наша команда реализует для них очередной метод для отдачи чего-либо. POData дает именно то, что надо — готовый инструмент для произвольных выборок, поэтому лично меня не смущает на данный момент поддержка только GET.
      Другие инструменты мне тоже интересны. Как в Apigility реализована фильтрация по произвольным условиям? Я по докам вижу только то, что можно задать набор строгих равенств, которые будут объединены только через логическое И: что-то вроде localhost:8000/books?title=php&sort=year
      Ну и OData — стандарт и под него подстроились некоторые UI библиотеки, что тоже на мой взгляд очень привлекательно.