Skip to content

Commit dba938f

Browse files
committedJul 10, 2024
Initial commit
0 parents  commit dba938f

27 files changed

+2993
-0
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea
2+
.phpunit.result.cache
3+
vendor

‎README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ExampleCom client
2+
3+
## Часть 1:
4+
5+
Задача:
6+
> Есть список директорий неизвестно насколько большой вложенности
7+
> В директории может быть файл count
8+
> Нужно пройтись по всем директориям и вернуть сумму всех чисел из файла count (файлов count может быть много)
9+
10+
Пример: `examples/part_1.php`
11+
12+
## Часть 2:
13+
14+
Задача:
15+
> Необходимо реализовать клиент для абстрактного (вымышленного) сервиса комментариев "example.com". Проект должен представлять класс или набор классов, который будет делать http запросы к серверу.
16+
> На выходе должна получиться библиотека, который можно будет подключить через composer к любому другому проекту.
17+
> У этого сервиса есть 3 метода:
18+
>
19+
> GET http://example.com/comments - возвращает список комментариев
20+
>
21+
> POST http://example.com/comment - добавить комментарий.
22+
>
23+
> PUT http://example.com/comment/{id} - по идентификатору комментария обновляет поля, которые были в в запросе
24+
>
25+
> Объект comment содержит поля:
26+
> id - тип int. Не нужно указывать при добавлении.
27+
> name - тип string.
28+
> text - тип string.
29+
>
30+
> Написать phpunit тесты, на которых будет проверяться работоспособность клиента.
31+
> Сервер example.com писать не надо! Только библиотеку для работы с ним.
32+
33+
Пример: `examples/part_2.php`
34+
35+
# Lint & Tests:
36+
37+
```bash
38+
docker compose up
39+
```
40+
41+
# Setup
42+
43+
```bash
44+
composer require flexnst/example-com-client
45+
```

‎compose.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
composer:
3+
image: composer:2
4+
pull_policy: always
5+
restart: no
6+
volumes:
7+
- '.:/app'
8+
working_dir: '/app'
9+
entrypoint: [ "/bin/bash", "-c" ]
10+
command:
11+
- |
12+
composer install --ignore-platform-reqs
13+
vendor/bin/phpstan analyse examples src tests
14+
echo -e "\n=== Задача №1: ==="
15+
php examples/part_1.php
16+
echo -e "\n\n=== Задача №2: ==="
17+
vendor/bin/phpunit -c phpunit.xml --testsuite Unit
18+
vendor/bin/phpunit -c phpunit.xml --testsuite Feature

‎composer.json

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "flexnst/example-com-client",
3+
"description": "Example.com comments client",
4+
"version": "1.0.0",
5+
"type": "library",
6+
"keywords": [
7+
"tz",
8+
"example.com",
9+
"comments",
10+
"client"
11+
],
12+
"authors": [
13+
{
14+
"name": "Alexandr Chernov",
15+
"email": "flex.nst@gmail.com"
16+
}
17+
],
18+
"require": {
19+
"php": "8.2",
20+
"guzzlehttp/guzzle": "^7.0"
21+
},
22+
"require-dev": {
23+
"phpunit/phpunit": "^10.0",
24+
"phpstan/phpstan": "^1.11"
25+
},
26+
"autoload": {
27+
"psr-4": {
28+
"ExampleCom\\": "src/"
29+
}
30+
},
31+
"autoload-dev": {
32+
"psr-4": {
33+
"Tests\\": "tests/"
34+
}
35+
},
36+
"config": {
37+
"optimize-autoloader": true,
38+
"preferred-install": "dist",
39+
"sort-packages": true
40+
},
41+
"prefer-stable": true
42+
}

‎composer.lock

+2,300
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎examples/part_1.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
function getSumFromFiles(string $path, string $filename): float
6+
{
7+
$sum = 0;
8+
9+
foreach (new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS) as $file) {
10+
/** @var SplFileInfo $file */
11+
if ($file->isDir()) {
12+
$sum += getSumFromFiles($file->getPathname(), $filename);
13+
continue;
14+
}
15+
16+
if ($file->getFilename() !== $filename) {
17+
continue;
18+
}
19+
20+
if (!$file->isReadable()) {
21+
echo "File {$file->getPathname()} is not readable\n";
22+
continue;
23+
}
24+
25+
$contents = file_get_contents($file->getPathname());
26+
27+
if (is_numeric($contents)) {
28+
$sum += (float) $contents;
29+
}
30+
}
31+
32+
return $sum;
33+
}
34+
35+
echo getSumFromFiles(__DIR__ . '/part_1_dir', 'count');

‎examples/part_1_dir/count

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1

‎examples/part_1_dir/dir1/count

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

‎examples/part_1_dir/dir1/dir2/count

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A

‎examples/part_1_dir/nocount

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
123

‎examples/part_2.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Psr\Http\Client\ClientExceptionInterface;
6+
7+
$psr7httpClient = new \GuzzleHttp\Client([
8+
'base_uri' => 'https://example.com',
9+
]);
10+
11+
$exampleCom = new \ExampleCom\Client($psr7httpClient);
12+
13+
/**
14+
* 1. Получить список комментариев
15+
*/
16+
try {
17+
$comments = $exampleCom->getComments();
18+
} catch (ClientExceptionInterface $exception) {
19+
// Исключения оставлены на этом уровне для более гибкой обработки ошибок
20+
// на уровне приложения, использующего данный пакет
21+
// $logger->error($exception->getMessage(), ...);
22+
}
23+
24+
/**
25+
* 2. Добавить комментарий
26+
*/
27+
try {
28+
$exampleCom->addComment('Ivanov Ivan', 'Lorem ipsum dolor.');
29+
} catch (ClientExceptionInterface $exception) {
30+
// $logger->error($exception->getMessage(), ...);
31+
}
32+
33+
/**
34+
* 3. Обновить комментарий
35+
*/
36+
try {
37+
$exampleCom->updateComment(123, 'Ivan Ivanovitch', 'Lorem ipsum dolor sit amet...');
38+
} catch (ClientExceptionInterface $exception) {
39+
// $logger->error($exception->getMessage(), ...);
40+
} catch (\ExampleCom\Exception\UpdateCommentException $exception) {
41+
// $logger->error($exception->getMessage(), ...);
42+
}
43+
44+

‎phpstan.neon

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
parameters:
2+
paths:
3+
- examples/
4+
- src/
5+
- tests/
6+
level: 9

‎phpunit.xml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
3+
<testsuites>
4+
<testsuite name="Feature">
5+
<directory suffix="Test.php">./tests/Feature</directory>
6+
</testsuite>
7+
<testsuite name="Unit">
8+
<directory suffix="Test.php">./tests/Unit</directory>
9+
</testsuite>
10+
</testsuites>
11+
<coverage/>
12+
<php/>
13+
<source>
14+
<include>
15+
<directory suffix=".php">./src</directory>
16+
</include>
17+
</source>
18+
</phpunit>

‎src/Client.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom;
6+
7+
use ExampleCom\Exception\UpdateCommentException;
8+
use ExampleCom\Request\AddCommentRequest;
9+
use ExampleCom\Request\GetCommentsRequest;
10+
use ExampleCom\Request\UpdateCommentRequest;
11+
use ExampleCom\Response\GetCommentsResponse;
12+
use JsonException;
13+
use Psr\Http\Client\ClientExceptionInterface;
14+
use Psr\Http\Client\ClientInterface;
15+
16+
class Client
17+
{
18+
public function __construct(
19+
private readonly ClientInterface $httpClient,
20+
) {
21+
}
22+
23+
/**
24+
* @return array<Comment>
25+
* @throws ClientExceptionInterface
26+
*/
27+
public function getComments(): array
28+
{
29+
$response = $this->httpClient->sendRequest(new GetCommentsRequest());
30+
31+
return (new GetCommentsResponse($response))->getComments();
32+
}
33+
34+
/**
35+
* @throws ClientExceptionInterface
36+
* @throws JsonException
37+
*/
38+
public function addComment(string $name, string $text): bool
39+
{
40+
$response = $this->httpClient
41+
->sendRequest(new AddCommentRequest(new Comment($name, $text)));
42+
43+
return $response->getStatusCode() === 200;
44+
}
45+
46+
/**
47+
* @throws ClientExceptionInterface
48+
* @throws UpdateCommentException
49+
* @throws JsonException
50+
*/
51+
public function updateComment(int $commentId, string $name, string $text): bool
52+
{
53+
$response = $this->httpClient
54+
->sendRequest(new UpdateCommentRequest(new Comment($name, $text, $commentId)));
55+
56+
return $response->getStatusCode() === 200;
57+
}
58+
}

‎src/Comment.php

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom;
6+
7+
class Comment
8+
{
9+
public function __construct(
10+
public string $name,
11+
public string $text,
12+
public ?int $id = null,
13+
) {
14+
}
15+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom\Exception;
6+
7+
use ErrorException;
8+
9+
class UpdateCommentException extends ErrorException
10+
{
11+
}

‎src/Request/AddCommentRequest.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom\Request;
6+
7+
use ExampleCom\Comment;
8+
use GuzzleHttp\Psr7\Request;
9+
use Psr\Http\Message\RequestInterface;
10+
11+
class AddCommentRequest extends Request implements RequestInterface
12+
{
13+
public function __construct(Comment $comment)
14+
{
15+
$json = json_encode([
16+
'name' => $comment->name,
17+
'text' => $comment->text,
18+
], JSON_THROW_ON_ERROR);
19+
20+
parent::__construct('POST', '/comment', [], $json);
21+
}
22+
}

‎src/Request/GetCommentsRequest.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom\Request;
6+
7+
use GuzzleHttp\Psr7\Request;
8+
use Psr\Http\Message\RequestInterface;
9+
10+
class GetCommentsRequest extends Request implements RequestInterface
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct('GET', '/comments');
15+
}
16+
}

‎src/Request/UpdateCommentRequest.php

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom\Request;
6+
7+
use ExampleCom\Comment;
8+
use ExampleCom\Exception\UpdateCommentException;
9+
use GuzzleHttp\Psr7\Request;
10+
use JsonException;
11+
use Psr\Http\Message\RequestInterface;
12+
13+
class UpdateCommentRequest extends Request implements RequestInterface
14+
{
15+
/**
16+
* @throws UpdateCommentException
17+
* @throws JsonException
18+
*/
19+
public function __construct(Comment $comment)
20+
{
21+
if (!is_int($comment->id)) {
22+
throw new UpdateCommentException('Comment id must be an integer.');
23+
}
24+
25+
$json = json_encode([
26+
'name' => $comment->name,
27+
'text' => $comment->text,
28+
], JSON_THROW_ON_ERROR);
29+
30+
parent::__construct('PUT', "/comment/{$comment->id}", [], $json);
31+
}
32+
}

‎src/Response/GetCommentsResponse.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ExampleCom\Response;
6+
7+
use ExampleCom\Comment;
8+
use Psr\Http\Message\ResponseInterface;
9+
10+
class GetCommentsResponse
11+
{
12+
public function __construct(
13+
private readonly ResponseInterface $response
14+
) {
15+
}
16+
17+
/**
18+
* @return array<int, Comment>
19+
*/
20+
public function getComments(): array
21+
{
22+
$this->response->getBody()->rewind();
23+
/** @var array<int, array<string, string|int>> $json */
24+
$json = json_decode($this->response->getBody()->getContents(), true);
25+
$comments = [];
26+
27+
foreach ($json as $comment) {
28+
$comments[] = new Comment(
29+
(string) $comment['name'],
30+
(string) $comment['text'],
31+
(int) $comment['id'],
32+
);
33+
}
34+
35+
return $comments;
36+
}
37+
}

‎tests/Feature/ClientTest.php

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Feature;
6+
7+
use ExampleCom\Client;
8+
use ExampleCom\Comment;
9+
use GuzzleHttp\Psr7\Response;
10+
use PHPUnit\Framework\TestCase;
11+
use Psr\Http\Client\ClientInterface;
12+
13+
class ClientTest extends TestCase
14+
{
15+
public function testAddCommentSuccess(): void
16+
{
17+
$exampleCom = $this->mockClient(new Response(200, [], ''));
18+
19+
self::assertTrue($exampleCom->addComment('Alice', 'Foo'));
20+
}
21+
22+
public function testAddCommentError(): void
23+
{
24+
$exampleCom = $this->mockClient(new Response(400, [], ''));
25+
26+
self::assertFalse($exampleCom->addComment('Alice', 'Foo'));
27+
}
28+
29+
public function testGetComments(): void
30+
{
31+
$exampleCom = $this->mockClient(new Response(200, [], (string) json_encode([
32+
[
33+
'id' => 1,
34+
'name' => 'Alice',
35+
'text' => 'Foo',
36+
],
37+
[
38+
'id' => 2,
39+
'name' => 'Bob',
40+
'text' => 'Bar',
41+
]
42+
])));
43+
44+
$comments = $exampleCom->getComments();
45+
46+
// Проверим кол-во элементов ответа
47+
self::assertEquals(2, count($comments));
48+
49+
// Проверим что первый элемент соответсвует Mock
50+
self::assertEquals($comments[0], new Comment(
51+
'Alice',
52+
'Foo',
53+
1
54+
));
55+
56+
// Проверим что второй элемент соответсвует Mock
57+
self::assertEquals($comments[1], new Comment(
58+
'Bob',
59+
'Bar',
60+
2
61+
));
62+
}
63+
64+
public function testUpdateComment(): void
65+
{
66+
$exampleCom = $this->mockClient(new Response(200, [], ''));
67+
68+
self::assertTrue($exampleCom->updateComment(1,'Alice', 'Foo'));
69+
}
70+
71+
private function mockClient(Response $response): Client
72+
{
73+
$httpClientMock = $this->createMock(ClientInterface::class);
74+
$httpClientMock->method('sendRequest')->willReturn($response);
75+
76+
return new Client($httpClientMock);
77+
}
78+
}

‎tests/Unit/AddCommentRequestTest.php

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use ExampleCom\Comment;
8+
use ExampleCom\Request\AddCommentRequest;
9+
use GuzzleHttp\Psr7\Uri;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class AddCommentRequestTest extends TestCase
13+
{
14+
/**
15+
* @dataProvider commentsDataProvider
16+
*/
17+
public function testAddCommentRequest(string $name, string $text): void
18+
{
19+
$request = new AddCommentRequest(new Comment(
20+
$name,
21+
$text,
22+
));
23+
24+
self::assertEquals('POST', $request->getMethod());
25+
self::assertEquals(new Uri('/comment'), $request->getUri()->getPath());
26+
27+
$request->getBody()->rewind();
28+
29+
self::assertEquals(
30+
'{"name":"' . $name . '","text":"' . $text . '"}',
31+
$request->getBody()->getContents()
32+
);
33+
}
34+
35+
/**
36+
* @return array<int, array<int, string>>
37+
*/
38+
public static function commentsDataProvider(): array
39+
{
40+
return [
41+
['name1', 'my comment'],
42+
['Alice', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'],
43+
['Bob', 'Hello!'],
44+
];
45+
}
46+
}

‎tests/Unit/CommentTest.php

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use ExampleCom\Comment;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class CommentTest extends TestCase
11+
{
12+
/**
13+
* @param int|null $id
14+
* @param string $name
15+
* @param string $text
16+
* @dataProvider commentsDataProvider
17+
*/
18+
public function testComment(?int $id, string $name, string $text): void
19+
{
20+
$comment = new Comment($name, $text, $id);
21+
22+
self::assertEquals($id, $comment->id);
23+
self::assertEquals($name, $comment->name);
24+
self::assertEquals($text, $comment->text);
25+
}
26+
27+
/**
28+
* @return array<int, array<int, int|null|string>>
29+
*/
30+
public static function commentsDataProvider(): array
31+
{
32+
return [
33+
[null, 'Vasia', 'Predev'],
34+
[1, 'Ivan', 'Lorem ipsum'],
35+
[99999, 'Alice', 'Lorem ipsum dolor sit amet'],
36+
];
37+
}
38+
}

‎tests/Unit/GetCommentsRequestTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use ExampleCom\Request\GetCommentsRequest;
8+
use GuzzleHttp\Psr7\Uri;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class GetCommentsRequestTest extends TestCase
12+
{
13+
public function testGetCommentsRequest(): void
14+
{
15+
$request = new GetCommentsRequest();
16+
17+
self::assertEquals('GET', $request->getMethod());
18+
self::assertEquals(new Uri('/comments'), $request->getUri()->getPath());
19+
20+
$request->getBody()->rewind();
21+
22+
self::assertEquals('', $request->getBody()->getContents());
23+
}
24+
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use ExampleCom\Comment;
8+
use ExampleCom\Response\GetCommentsResponse;
9+
use GuzzleHttp\Psr7\Response;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class GetCommentsResponseTest extends TestCase
13+
{
14+
/**
15+
* @param string $jsonResponse
16+
* @param array<int, Comment> $comments
17+
* @dataProvider commentsDataProvider
18+
*/
19+
public function testGetCommentsResponse(string $jsonResponse, array $comments): void
20+
{
21+
$response = new GetCommentsResponse(new Response(200, [], $jsonResponse));
22+
23+
self::assertEquals($response->getComments(), $comments);
24+
}
25+
26+
/**
27+
* @return array<int, array<int, string|array<int, Comment>>>
28+
*/
29+
public static function commentsDataProvider(): array
30+
{
31+
return [
32+
[
33+
'[{"id":1,"name":"aaa","text":"comment 1"},{"id":2,"name":"bbb","text":"comment 2"}]',
34+
[
35+
new Comment('aaa', 'comment 1', 1),
36+
new Comment('bbb', 'comment 2', 2),
37+
]
38+
],
39+
[
40+
'[{"id":3,"name":"ccc","text":"comment 3"},{"id":4,"name":"ddd","text":"comment 4"}]',
41+
[
42+
new Comment('ccc', 'comment 3', 3),
43+
new Comment('ddd', 'comment 4', 4),
44+
]
45+
],
46+
];
47+
}
48+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use ExampleCom\Comment;
8+
use ExampleCom\Exception\UpdateCommentException;
9+
use ExampleCom\Request\UpdateCommentRequest;
10+
use GuzzleHttp\Psr7\Uri;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class UpdateCommentRequestTest extends TestCase
14+
{
15+
/**
16+
* @dataProvider commentDataProvider
17+
*/
18+
public function testAddCommentRequest(string $name, string $text, ?int $commentId): void
19+
{
20+
if ($commentId === null) {
21+
$this->expectException(UpdateCommentException::class);
22+
}
23+
24+
$request = new UpdateCommentRequest(new Comment(
25+
$name,
26+
$text,
27+
$commentId
28+
));
29+
30+
self::assertEquals('PUT', $request->getMethod());
31+
self::assertEquals(new Uri("/comment/{$commentId}"), $request->getUri()->getPath());
32+
33+
$request->getBody()->rewind();
34+
35+
self::assertEquals(
36+
'{"name":"' . $name . '","text":"' . $text . '"}',
37+
$request->getBody()->getContents()
38+
);
39+
}
40+
41+
/**
42+
* @return array<int, array<int, int|string|null>>
43+
*/
44+
public static function commentDataProvider(): array
45+
{
46+
return [
47+
['name1', 'my comment', 1],
48+
['Alice', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', null],
49+
['Bob', 'Hello!', 3],
50+
];
51+
}
52+
}

0 commit comments

Comments
 (0)
Please sign in to comment.