Я прочитал много рекомендаций по управлению версиями URI RESTful API, например (http://api.example.com/v1/users
-> http://api.example.com/v2/users
а также HATEOAS), но не очень о структуре каталогов или пространства имен в моей базе кода (PHP + silex framework).
Что может сделать моя база кода прямо сейчас: сама база кода поддерживает несколько версий API, распознает версию по маршруту или заголовок Accept и может вызывать различные контроллеры / классы / методы на основе определенной версии API (например, в v1 UserController::listUsers()
в v2: UserControllerV2::getListUsers()
).
Со временем API будет иметь все больше и больше версий, но в какой-то момент старые версии должны быть удалены из базы кода.
Итак, вопросы
Текущий src
структура каталогов, например, (public
, vendor
на один уровень выше):
.
└── TestNext
├── ApiV1
│ └── Route
│ └── ApiV1RoutesProvider.php
├── Configuration
│ ├── Controller
│ ├── Loader
│ │ └── YamlConfigLoader.php
│ ├── Model
│ └── Service
│ └── SymfonyConfigServiceProvider.php
├── Security
│ └── Authenticator
│ └── TokenAuthenticator.php
├── Shared
│ └── Controller
│ └── BaseController.php
├── User
│ ├── Controller
│ │ └── UserController.php
│ ├── Model
│ └── Service
├── Bootstrap.php
├── Console.php
└── Constants.php
Во-первых, стоит отделить понятия друг от друга. У вас есть свой домен, и у вас есть уровень API. После наслоения ваш уровень API должен находиться выше (и «отделяться») от вашего домена, и ваш домен должен полностью не знать о существовании API. Это помогает структурировать вещи вокруг этого, один из способов сделать это так:
src/Acme/Api/
src/Acme/Core/
Все в API обрабатывает коммуникации уровня HTTP; маршрутизация, запрос & отображение ответов, коды состояния и т. д.
Все в Core обрабатывает действия, связанные с бизнесом. Следуя подходу в стиле CQRS, вы можете получить что-то вроде:
src/Acme/Api/Controller
src/Acme/Api/DTO/Request/
src/Acme/Api/DTO/Response/
src/Core/Domain/
src/Core/Command/
src/Core/CommandHandler/
src/Core/Infrastructure/
src/Core/ReadModel/
Но на самом деле, макет & Именование будет гибким, и это в некоторой степени зависит от того, какие архитектурные шаблоны вы применяете. В контексте DDD ключевым моментом является то, что вы размещаете свои агрегаты, модели, объекты стоимости. & хранилища вместе под некоторым общим пространством имен (Domain
).
Чтобы ответить на ваши индивидуальные вопросы:
Какие классы должны быть версионными? (контроллеры, модели, виды и т. д.)
На мой взгляд это не имеет смысла для версии моделей. Модели всегда должны быть актуальным представлением бизнеса и поддерживать старшая правила кажутся ненужными.
Как вы справляетесь с версионированием, зависит от вас. Вы можете рассматривать его как способ создания версии API полностью (пути + полезные нагрузки запросов / ответов) или просто полезных нагрузок запросов / ответов. Управление версиями API в нем полностью, вероятно, наиболее гибкое, но внесение таких кардинальных изменений относительно редко. Вы можете рассмотреть возможность использования Accept
/ Content-Type
заголовки для версии на уровне запроса. Вы даже можете использовать комбинацию обоих (основной вариант версии в URL, чтобы переопределить пути и принудительно Content-Type
с версиями).
Теоретически вы можете пойти еще дальше и создать версию своей JSON-схемы, которая определяет полезные нагрузки вашего запроса / ответа. В качестве примера:
GET /1.0/user/317684e2-3704-11e6-8172-b0bea8105888/payment/2caad76e-3705-11e6-8172-b0bea8105888
Accept: application/vnd+acme+json; schema=payment.out.v1.json
HTTP/1.1 200 OK
Content-Type: application/vnd+acme+json; schema=payment.out.v1.json; charset=UTF-8
{
"payee": "Bob Dylan",
// snip
}
```
Если вы хотите внести не обратно совместимое изменение в полезную нагрузку ответа, вы можете разрешить клиентам запрашивать платежи по схеме JSON «v2»:
GET /1.0/user/317684e2-3704-11e6-8172-b0bea8105888/payment/2caad76e-3705-11e6-8172-b0bea8105888
Accept: application/vnd+acme+json; schema=payment.out.v2.json
HTTP/1.1 200 OK
Content-Type: application/vnd+acme+json; schema=payment.out.v2.json; charset=UTF-8
{
"payee": {
"forename": "Bob",
"surname": "Dylan"},
// snip
}
Основная версия API в неизменном виде, но ответные нагрузки различаются. Со временем вы можете отказаться от более старых версий или даже увеличить основную версию API и установить минимум на уровне полезной нагрузки.
Тот же самый метод может быть применен к телам запросов, используя Content-Type
заголовок.
Как это сделать, если у вас есть пакетный дизайн, управляемый доменом? (версия полного каталога пакетов или просто внутри пакета?)
Как они должны работать? (Класс наследования (Как?), Структура каталогов …)
Возможно, это уже получено, но постарайтесь не думать в терминах пакетов стиля Symfony. Ваш Core
или как вы хотите это назвать, в нем не должно быть ничего специфического для Symfony / Silex. В Infrastructure
у вас могут быть реализации Repository
интерфейсы, возможно, с использованием Doctrine, но мы используем Doctrine как библиотеку, а не опираемся на Symfony / Silex в качестве фреймворка. Если вы делаете это хорошо, теоретически вы можете поменять слой API на совершенно другую структуру без каких-либо изменений в Core
,
Однако есть пара неизбежных вещей: внедрение зависимости & конфигурации. С Symfony я создал CoreBundle
чтобы решить это в прошлом. Это сидит за пределами Core
,
Кстати, возможно, разумно охватить только один ограниченный контекст внутри такого проекта, поэтому не стоит беспокоиться о том, чтобы классифицировать вещи намного дальше.
С меньшим дублированием кода,
Дублирование неизбежно, но когда это должно иметь место только для ваших DTO, так как вы не будете управлять версиями объектов, определяющих поведение (ваши модели). Дублирование в DTO не имеет большого значения. Если больше ясности получено, чем потеряно, тогда просто определите класс для версии. Инструменты могут сказать, что код был скопирован, но это не инструменты, способные понимать такой контекст.
легкое удаление старой версии
Если вы определяете отдельные DTO, то такая задача должна быть простой. Удалите DTO, удалите схему JSON, и теперь ваш API должен отклонять запросы с указанием более старых версий.
меньше побочных эффектов, если вы исправляете что-то общее между версиями
Если вы сопоставляете объекты запроса / ответа на уровне API, то это — ваш код сопоставления, на который могут повлиять изменения в моделях. Хороший охват тестами (через контрактные API-тесты, внутрипроцессные тесты компонентов и модульные тесты) должен проверить, что изменения в ваших моделях не приводят к тому, что все версии вашего API ведут себя по-разному.
Других решений пока нет …