├── README.md ├── document-list-response.json ├── document-response.json ├── document.json └── img └── logo.png /README.md: -------------------------------------------------------------------------------- 1 | # Тестовое задание для кандидатов на вакансию Backend (PHP) разработчика 2 | 3 | ![MagDevelopment](./img/logo.png) 4 | 5 | [МАГ Девелопмент](http://magdv.com) — это компания, где создаются инновационные сервисы на стыке информационных технологий, транспортной логистики и торговли. 6 | 7 | Наши вакансии на сайте [http://magdv.com](http://magdv.com). 8 | 9 | ## Задание 10 | 11 | Необходимо реализовать систему для редактирования любого json-документа методом PATCH. 12 | Клиент системы должен иметь возможность создать пустой черновик документа. 13 | Пока документ находится в статусе черновик, его можно редактировать сколько угодно раз. 14 | Черновик документа можно опубликовать. 15 | После публикации документ больше редактировать нельзя. 16 | 17 | Данное задание может считаться обучающим. 18 | Мы не ожидаем что вы являетесь опытным программистом, однако если вы готовы 19 | изучить технологии и выполнить это задание - мы ждем вас к нам в стажеры. 20 | 21 | ## Требования 22 | 23 | | | | 24 | |------------------|---------------------------------------------------------| 25 | | Время исполнения | 8-16ч опытным разработчиком | 26 | | Фреймворк | Yii2, Zend, Laravel, Symfony, свой вариант вокруг PSR-* | 27 | | Сторонние пакеты | любые | 28 | | Php | >=7.1 | 29 | | Db | postgres, mysql | 30 | 31 | 1. Разместить код на любом доступном git-резпозитории. 32 | 2. Описать файл `README.md`, описать как запустить проект. 33 | 3. Соблюдать единый code-style на протяжении всего проекта 34 | 4. Обязательна документация для каждого метода, класса и поля. Указание типов обязательно. 35 | 5. Первый коммит в проекте должен быть - настройка и конфигурация фреймворка (скелета). 36 | 6. Отчет в виде затраченного времени, полнота исполнения задания, а также, возникшие проблемы сложности и их решения, пожелания, комментарий и пр. 37 | 38 | ## API 39 | 40 | - `POST /api/v1/document/` - создаем черновик документа 41 | - `GET /api/v1/document/{id}` - получить документ по id 42 | - `PATCH /api/v1/document/{id}` - редактировать документ 43 | - `POST /api/v1/document/{id}/publish` - опубликовать документ 44 | - `GET /api/v1/document/?page=1&perPage=20` - получить список документов с пагинацией, сортировка в последние созданные сверху. 45 | 46 | Дополнительные условия: 47 | 48 | - Если документ не найден, то в ответе возвращается 404 код. 49 | - При попытке редактирования документа, который уже опубликован, должно возвращаться 400. 50 | - Попытка опубликовать уже опубликованный документ возвращает 200. 51 | - Все запросы на конкретный документ возвращают этот документ. [JsonSchema ответа с документом](document-response.json). 52 | - Список документов возвращается в виде массива документов и значений пагинации. [JsonSchema списка документов](document-list-response.json). 53 | - Запрос `PATCH` отправляется с телом json в соответсвующей иерархии документа, все поля, кроме `payload` игнорируются. Если `payload` не передан, то ответ 400. 54 | 55 | ### Объект документа 56 | 57 | ```js 58 | document = { 59 | id: "some-uuid-string", 60 | status: "draft|published", 61 | payload: Object, 62 | createAt: "iso-8601 date time with time zone", 63 | modifyAt: "iso-8601 date time with time zone" 64 | } 65 | ``` 66 | 67 | [JsonSchema для документа](document.json) 68 | 69 | ## Патчинг документа 70 | 71 | Патчинг проводится согласно [RFC-7396](https://tools.ietf.org/html/rfc7396). 72 | 73 | ## Пример работы 74 | 75 | ### 1. Клиент делает запрос на создание документа 76 | 77 | Запрос: 78 | 79 | ```http 80 | POST /api/v1/document HTTP/1.1 81 | accept: application/json 82 | ``` 83 | 84 | Ответ: 85 | 86 | ```http 87 | HTTP/1.1 200 OK 88 | content-type: application/json 89 | 90 | { 91 | "document": { 92 | "id": "718ce61b-a669-45a6-8f31-32ba41f94784", 93 | "status": "draft", 94 | "payload": {}, 95 | "createAt": "2018-09-01 20:00:00+07:00", 96 | "modifyAt": "2018-09-01 20:00:00+07:00" 97 | } 98 | } 99 | ``` 100 | 101 | ### 2. Клиент редактирует документ первый раз 102 | 103 | Запрос: 104 | 105 | ```http 106 | PATCH /api/v1/document/718ce61b-a669-45a6-8f31-32ba41f94784 HTTP/1.1 107 | accept: application/json 108 | content-type: application/json 109 | 110 | { 111 | "document": { 112 | "payload": { 113 | "actor": "The fox", 114 | "meta": { 115 | "type": "quick", 116 | "color": "brown" 117 | }, 118 | "actions": [ 119 | { 120 | "action": "jump over", 121 | "actor": "lazy dog" 122 | } 123 | ] 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | Ответ: 130 | 131 | ```http 132 | HTTP/1.1 200 OK 133 | content-type: application/json 134 | 135 | { 136 | "document": { 137 | "id": "718ce61b-a669-45a6-8f31-32ba41f94784", 138 | "status": "draft", 139 | "payload": { 140 | "actor": "The fox", 141 | "meta": { 142 | "type": "quick", 143 | "color": "brown" 144 | }, 145 | "actions": [ 146 | { 147 | "action": "jump over", 148 | "actor": "lazy dog" 149 | } 150 | ] 151 | }, 152 | "createAt": "2018-09-01 20:00:00+07:00", 153 | "modifyAt": "2018-09-01 20:01:00+07:00" 154 | } 155 | } 156 | ``` 157 | 158 | ### 3. Клиент редактирует документ 159 | 160 | Запрос: 161 | 162 | ```http 163 | PATCH /api/v1/document/718ce61b-a669-45a6-8f31-32ba41f94784 HTTP/1.1 164 | accept: application/json 165 | content-type: application/json 166 | 167 | { 168 | "document": { 169 | "payload": { 170 | "meta": { 171 | "type": "cunning", 172 | "color": null 173 | }, 174 | "actions": [ 175 | { 176 | "action": "eat", 177 | "actor": "blob" 178 | }, 179 | { 180 | "action": "run away" 181 | } 182 | ] 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | Ответ: 189 | 190 | ```http 191 | HTTP/1.1 200 OK 192 | content-type: application/json 193 | 194 | { 195 | "document": { 196 | "id": "718ce61b-a669-45a6-8f31-32ba41f94784", 197 | "status": "draft", 198 | "payload": { 199 | "actor": "The fox", 200 | "meta": { 201 | "type": "cunning", 202 | }, 203 | "actions": [ 204 | { 205 | "action": "eat", 206 | "actor": "blob" 207 | }, 208 | { 209 | "action": "run away" 210 | } 211 | ] 212 | }, 213 | "createAt": "2018-09-01 20:00:00+07:00", 214 | "modifyAt": "2018-09-01 20:02:00+07:00" 215 | } 216 | } 217 | ``` 218 | 219 | ### 4. Клиент публикует документ 220 | 221 | Запрос: 222 | 223 | ```http 224 | POST /api/v1/document/718ce61b-a669-45a6-8f31-32ba41f94784/publish HTTP/1.1 225 | accept: application/json 226 | ``` 227 | 228 | Ответ: 229 | 230 | ```http 231 | HTTP/1.1 200 OK 232 | content-type: application/json 233 | 234 | { 235 | "document": { 236 | "id": "718ce61b-a669-45a6-8f31-32ba41f94784", 237 | "status": "published", 238 | "payload": { 239 | "actor": "The fox", 240 | "meta": { 241 | "type": "cunning", 242 | }, 243 | "actions": [ 244 | { 245 | "action": "eat", 246 | "actor": "blob" 247 | }, 248 | { 249 | "action": "run away" 250 | } 251 | ] 252 | }, 253 | "createAt": "2018-09-01 20:00:00+07:00", 254 | "modifyAt": "2018-09-01 20:03:00+07:00" 255 | } 256 | } 257 | ``` 258 | 259 | ### 5. Клиент получает запись в списке 260 | 261 | Запрос: 262 | 263 | ```http 264 | GET /api/v1/document/?page=1 HTTP/1.1 265 | accept: application/json 266 | ``` 267 | 268 | Ответ: 269 | 270 | ```http 271 | HTTP/1.1 200 OK 272 | content-type: application/json 273 | 274 | { 275 | "document": [ 276 | { 277 | "id": "718ce61b-a669-45a6-8f31-32ba41f94784", 278 | "status": "published", 279 | "payload": { 280 | "actor": "The fox", 281 | "meta": { 282 | "type": "cunning", 283 | }, 284 | "actions": [ 285 | { 286 | "action": "eat", 287 | "actor": "blob" 288 | }, 289 | { 290 | "action": "run away" 291 | } 292 | ] 293 | }, 294 | "createAt": "2018-09-01 20:00:00+07:00", 295 | "modifyAt": "2018-09-01 20:03:00+07:00" 296 | } 297 | ], 298 | "pagination": { 299 | "page": 1, 300 | "perPage": 20, 301 | "total": 1 302 | } 303 | } 304 | ``` 305 | 306 | ## Опциональные задания 307 | 308 | Опциональные задания не являются обязательными, однако, если вы не будете испытывать сложности с их реализацией, то, вероятно, вы опытный разработчик. Если же сложности все-таки возникают, то значит есть повод их преодолеть и получить новые знания. В задачах ниже описаны примерные задания. Полнота и способ их исполнения - целиком продукт вашего творчества, ведь программист - творческая профессия. 309 | 310 | Попробуйте реализовать хотя бы одну задачу. Даже если у вас не получиться, мы будем рады тому что вы попробовали. Опциональные задания повысят вас в наших глазах как разработчика. 311 | 312 | ### Написать unit/api тесты 313 | 314 | Необходимо описать минимальный набор тестов, для того чтобы убедиться что ваше приложение работоспособно. Желательно использовать php-unit или codeception. 315 | 316 | ### Декомпозиция задач 317 | 318 | Составить план работ/модулей которые надо реализовать. Дать оценку времени, которое будет затрачено на каждый из пунктов. Оформить каждый пункт как отдельный коммит. По окончанию реализации задачи записать сколько реально времени было потрачено. 319 | 320 | В итоге получить примерно такую таблицу: 321 | 322 | | # | Задача | Оценка | Затрачено | Комментарий | 323 | |:--:|:---------------------|:------:|:---------:|:--------------------------| 324 | | 1 | Настройка окружения | 1ч | 40м | Нашел хорошую инструкцию | 325 | | 2 | Установка фрэймворка | 20м | 30м | Забыл установить composer | 326 | | 3 | ............ | ... | ... | ... | 327 | 328 | ### Docker 329 | 330 | Завернуть ваше приложение в Docker контейнер. Написать скрипт разворачивания приложения одной командой. 331 | 332 | Самый удобный способ для этого, конечно, docker-compose. 333 | 334 | ### Добавить фронт 335 | 336 | - Добавить возможность получения списка документов с пагинатором по пути `/` 337 | - Просмотр конкретного документа по пути `/document/{id}`. 338 | 339 | Внешний вид нас не интересует - поэтому не стоит пытаться сделать все идеально. Важен способ реализации и время которое вы на это затратите. Лучше использовать готовые решения и фреймворки. Ограничений нет - полная свобода творчества. 340 | 341 | ### Реализовать патчинг самостоятельно 342 | 343 | В вашем приложении вы могли использовать готовый модуль для патча. Если это так, то попробуйте реализовать алгоритм патчинга самостоятельно. 344 | 345 | ### Добавить аутентификацию/авторизацию 346 | 347 | Реализовать авторизацию без пароля `POST /api/v1/login` 348 | Запрос: 349 | 350 | ```http 351 | POST /api/v1/login HTTP/1.1 352 | accept: application/json 353 | content-type: application/json 354 | 355 | { 356 | "login": "root" 357 | } 358 | ``` 359 | 360 | Ответ: 361 | 362 | ```http 363 | HTTP/1.1 200 OK 364 | content-type: application/json 365 | 366 | { 367 | "user": "root", 368 | "token": "q56lVCW9aIW6Gs01F5N9raaQordCb8HW", 369 | "until": 1537352295 370 | } 371 | ``` 372 | 373 | token - это случайная строка из символов. 374 | until - это unix timestamp до которого действует токен. 375 | 376 | Аутентификация должна проходить по заголовку в запросе. Например: 377 | 378 | ```http 379 | GET /api/v1/document/718ce61b-a669-45a6-8f31-32ba41f94784 HTTP/1.1 380 | accept: application/json 381 | Authorization: bearer q56lVCW9aIW6Gs01F5N9raaQordCb8HW 382 | ``` 383 | 384 | Требования: 385 | 386 | 1. Каждый раз когда пользователь авторизуется - он получает новый токен. 387 | 2. Если пользователь шлет любой запрос с несуществующим токеном или с просроченным токеном - то должен вернуться ответ с кодом 401. 388 | 3. Аноним может видеть список опубликованных документов, а также загружать конкретный опубликованный документ. 389 | 4. Аноним при попытке обратиться к PATCH и POST запросам будет получать ошибку 401. 390 | 5. Документ может создать только пользователь. 391 | 6. Редактировать и опубликовать документ может только пользователь создавший его. 392 | 7. Пользователь в списке документов видит только свои неопубликованные документы и опубликованные документы других пользователей, но не видит неопубликованные документы других. 393 | 8. Пользователь при попытке обратиться к чужому, неопубликованному документу получает 403. 394 | 9. Время действия токена - 1 час. 395 | 396 | ### Учесть конкурентные запросы 397 | 398 | Как будет вести себя ваше приложения с учетом попыток обновления одного документа несколькими клиентами одновременно? Если есть проблемы - надо их устранить. -------------------------------------------------------------------------------- /document-list-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "DocumentResponse", 4 | "description": "Ответ с документом", 5 | "type": "object", 6 | "additionalProperties": false, 7 | "properties": { 8 | "document": { 9 | "description": "Массив документов", 10 | "type": "array", 11 | "items": { 12 | "$ref": "document.json" 13 | } 14 | }, 15 | "pagination": { 16 | "type": "object", 17 | "required": [ 18 | "page", 19 | "perPage", 20 | "total" 21 | ], 22 | "properties": { 23 | "page": { 24 | "description": "Номер страницы", 25 | "type": "integer" 26 | }, 27 | "perPage": { 28 | "description": "Элементов на странице", 29 | "type": "integer" 30 | }, 31 | "total": { 32 | "description": "Всего документов", 33 | "type": "integer" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /document-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "DocumentResponse", 4 | "description": "Ответ с документом", 5 | "type": "object", 6 | "additionalProperties": false, 7 | "properties": { 8 | "document": { 9 | "description": "Документ", 10 | "$ref": "document.json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /document.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Document", 4 | "description": "Документ", 5 | "type": "object", 6 | "additionalProperties": false, 7 | "required": [ 8 | "id", 9 | "status", 10 | "payload", 11 | "createAt", 12 | "modifyAt" 13 | ], 14 | "properties": { 15 | "id": { 16 | "description": "uuid идентификатор", 17 | "type": "string" 18 | }, 19 | "status": { 20 | "description": "Статус документа", 21 | "type": "string", 22 | "enum": [ 23 | "draft", 24 | "published" 25 | ] 26 | }, 27 | "payload": { 28 | "description": "Тело документа", 29 | "type": "object" 30 | }, 31 | "createAt": { 32 | "description": "Дата создания документа в формате iso-8601 с часовой зоной", 33 | "type": "string" 34 | }, 35 | "modifyAt": { 36 | "description": "Дата последнего изменения документа в формате iso-8601 с часовой зоной", 37 | "type": "string" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magdv/php-test-task/94c0bb31a7bad9ba46ab460c19502b70af87e94d/img/logo.png --------------------------------------------------------------------------------