Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "project",
"license": "proprietary",
"require": {
"php": ">=8.4",
"php": ">=8.5",
"ext-pdo": "*",
"ext-redis": "*",
"ext-memcached": "*",
Expand Down
5 changes: 5 additions & 0 deletions config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@
['POST', '/add-event', \App\Controller\Web\EventController::class, 'addEvent'],
['GET', '/get-event', \App\Controller\Web\EventController::class, 'getEvent'],
['DELETE', '/delete-events', \App\Controller\Web\EventController::class, 'deleteEvents'],
['GET', '/get-user', \App\Controller\Web\User\UserController::class, 'getUser'],
['GET', '/get-users', \App\Controller\Web\User\UserController::class, 'getUsers'],
['POST', '/create-user', \App\Controller\Web\User\UserController::class, 'createUser'],
['PATCH', '/update-user-email', \App\Controller\Web\User\UserController::class, 'updateUserEmail'],
['DELETE', '/delete-user', \App\Controller\Web\User\UserController::class, 'deleteUser'],
];
13 changes: 13 additions & 0 deletions http/users/create_user.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
POST http://localhost:{{nginxPublishedPort}}/create-user
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache

{
"createUser": {
"firstName": "Иван",
"lastName": "Иванов",
"email": "i.ivanov@gmail.com",
"birthDate": "2005-03-05"
}
}
4 changes: 4 additions & 0 deletions http/users/delete_user.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DELETE http://localhost:{{nginxPublishedPort}}/delete-user?id=1
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache
4 changes: 4 additions & 0 deletions http/users/get_user.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GET http://localhost:{{nginxPublishedPort}}/get-user?id=1
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache
9 changes: 9 additions & 0 deletions http/users/get_users.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
GET http://localhost:{{nginxPublishedPort}}/get-users
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache

{
"lastId": 0,
"limit": 10
}
8 changes: 8 additions & 0 deletions http/users/update_user_email.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PATCH http://localhost:{{nginxPublishedPort}}/update-user-email?id=1
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache

{
"newEmail": "i.ivanov@mail.ru"
}
31 changes: 31 additions & 0 deletions src/Controller/Command/CreateUsersTableCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Controller\Command;

use App\Infrastructure\Database\Connection\PDOWrapper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand('db:users:create')]
class CreateUsersTableCommand extends Command
{
public function __invoke(OutputInterface $output): int
{
$columnDefinitionList = [
'id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY',
'first_name VARCHAR(50) NOT NULL',
'last_name VARCHAR(50) NOT NULL',
'email VARCHAR(255) NOT NULL',
'birth_date DATE NOT NULL',
'status BOOLEAN NOT NULL DEFAULT TRUE',
];

$sql = sprintf('CREATE TABLE users (%s)', implode(', ', $columnDefinitionList));
PDOWrapper::getHandler()->prepare($sql)->execute();

$output->writeln('Таблица пользователей успешно создана.');

return Command::SUCCESS;
}
}
31 changes: 31 additions & 0 deletions src/Controller/Web/User/CreateUserDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Controller\Web\User;

readonly class CreateUserDTO
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
public \DateTimeImmutable $birthDate,
) {
}

public static function fromArray(array $data): self
{
$data = $data['createUser'] ?? throw new \InvalidArgumentException('Неверное тело запроса.', 400);

$fields = array_keys(get_class_vars(self::class));
if (!empty(array_diff($fields, array_keys($data)))) {
throw new \InvalidArgumentException('Неверное тело запроса.', 400);
}

return new self(
firstName: $data['firstName'],
lastName: $data['lastName'],
email: $data['email'],
birthDate: \DateTimeImmutable::createFromFormat('Y-m-d', $data['birthDate']),
);
}
}
28 changes: 28 additions & 0 deletions src/Controller/Web/User/GetUsersDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Controller\Web\User;

readonly class GetUsersDTO
{
public function __construct(
public int $lastId = 0,
public int $limit = 10,
) {
}

public static function fromArray(array $data): self
{
if (isset($data['lastId']) && isset($data['limit'])) {
$lastId = $data['lastId'];
$limit = $data['limit'];
if (is_numeric($lastId) && is_numeric($limit)) {
return new self(
(int) $lastId,
(int) $limit,
);
}
}

throw new \InvalidArgumentException('Неверное тело запроса.', 400);
}
}
140 changes: 140 additions & 0 deletions src/Controller/Web/User/UserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

namespace App\Controller\Web\User;

use App\Application\Response;
use App\Controller\Web\AbstractController;
use App\Domain\Entity\User;
use App\Domain\Model\CreateUserModel;
use App\Domain\Model\GetUsersModel;
use App\Domain\Model\UpdateUserEmailModel;
use App\Domain\Service\UserService;
use Customer41\MultiException\MultiException;

class UserController extends AbstractController
{
private readonly UserService $userService;

public function __construct()
{
$this->userService = new UserService();
parent::__construct();
}

public function createUser(): Response
{
try {
$payload = $this->request->getPayload();
$createUser = CreateUserDTO::fromArray($payload);
$createdUser = $this->userService->createUser(
new CreateUserModel(
firstName: $createUser->firstName,
lastName: $createUser->lastName,
email: $createUser->email,
birthDate: $createUser->birthDate,
),
);
$data['success'] = true;
$data['user'] = $createdUser->toArray();
$httpCode = 200;
} catch (MultiException $validationErrors) {
$data['success'] = false;
$errors = [];
foreach ($validationErrors as $error) {
$errors[] = $error->getMessage();
}
$data['message'] = $validationErrors->getMessage();
$data['details'] = $errors;
$httpCode = $validationErrors->getCode();
} catch (\Throwable $e) {
$data['success'] = false;
$data['message'] = $e->getMessage();
$httpCode = $e->getCode();
}

return new Response(json_encode($data), $httpCode, ['Content-Type: application/json; charset=utf-8']);
}

public function getUser(): Response
{
try {
$id = $this->request->getQueryParam('id');
$id = is_numeric($id) ? (int) $id : throw new \Exception('Пользователь не найден.', 404);
$user = $this->userService->findUser($id);
$data['success'] = true;
$data['user'] = $user->toArray();
$httpCode = 200;
} catch (\Throwable $e) {
$data['success'] = false;
$data['message'] = $e->getMessage();
$httpCode = $e->getCode();
}

return new Response(json_encode($data), $httpCode, ['Content-Type: application/json; charset=utf-8']);
}

public function getUsers(): Response
{
try {
$payload = $this->request->getPayload();
$getUsers = GetUsersDTO::fromArray($payload);
$users = $this->userService->findUsers(
new GetUsersModel(
lastId: $getUsers->lastId,
limit: $getUsers->limit,
),
);

$data['success'] = true;
$data['users'] = array_map(static fn(User $user) => $user->toArray(), $users->toArray());
$data['pager'] = ['lastId' => $users->max('id'), 'totalItems' => $users->count()];
$httpCode = 200;
} catch (\Throwable $e) {
$data['success'] = false;
$data['message'] = $e->getMessage();
$httpCode = $e->getCode();
}

return new Response(json_encode($data), $httpCode, ['Content-Type: application/json; charset=utf-8']);
}

public function updateUserEmail(): Response
{
try {
$id = $this->request->getQueryParam('id');
$id = is_numeric($id) ? (int) $id : throw new \Exception('Пользователь не найден.', 404);
$newEmail = $this->request->getPayload()['newEmail']
?? throw new \Exception('Неверное тело запроса.', 400);
$updatedUser = $this->userService->updateUserEmail(
new UpdateUserEmailModel(userId: $id, newEmail: $newEmail)
);
$data['success'] = true;
$data['user'] = $updatedUser->toArray();
$httpCode = 200;
} catch (\Throwable $e) {
$data['success'] = false;
$data['message'] = $e->getMessage();
$httpCode = $e->getCode();
}

return new Response(json_encode($data), $httpCode, ['Content-Type: application/json; charset=utf-8']);
}

public function deleteUser(): Response
{
try {
$id = $this->request->getQueryParam('id');
$id = is_numeric($id) ? (int) $id : throw new \Exception('Пользователь не найден.', 404);
$this->userService->deleteUser($id);
$data['success'] = true;
$data['message'] = 'Пользователь успешно удалён.';
$httpCode = 200;
} catch (\Throwable $e) {
$data['success'] = false;
$data['message'] = $e->getMessage();
$httpCode = $e->getCode();
}

return new Response(json_encode($data), $httpCode, ['Content-Type: application/json; charset=utf-8']);
}
}
61 changes: 61 additions & 0 deletions src/Domain/Collection/AbstractCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Domain\Collection;

use App\Domain\Entity\EntityInterface;

abstract class AbstractCollection implements \IteratorAggregate, \Countable
{
private array $items = [];

abstract public function add(EntityInterface $entity): void;

public function getIterator(): \Traversable
{
return new \ArrayIterator($this->items);
}

public function count(): int
{
return count($this->items);
}

public function isEmpty(): bool
{
return empty($this->items);
}

public function toArray(): array
{
return $this->items;
}

public function max(string $field): mixed
{
if ($this->isEmpty()) {
return null;
}

$getter = 'get' . ucfirst($field);
$hasGetter = array_all($this->items, static fn(EntityInterface $entity, $k) => method_exists($entity, $getter));
if (!$hasGetter) {
return null;
}

$max = null;
foreach ($this->items as $entity) {
if ($max === null) {
$max = $entity->$getter();
continue;
}
$max = max($entity->$getter(), $max);
}

return $max;
}

protected function addEntity(EntityInterface $entity): void
{
$this->items[] = $entity;
}
}
21 changes: 21 additions & 0 deletions src/Domain/Collection/UserCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Domain\Collection;

use App\Domain\Entity\EntityInterface;
use App\Domain\Entity\User;

class UserCollection extends AbstractCollection
{
/**
* @param User $entity
*/
public function add(EntityInterface $entity): void
{
if (!$entity instanceof User) {
throw new \RuntimeException('Ожидается объект класса ' . User::class);
}

$this->addEntity($entity);
}
}
Loading