initial commit
This commit is contained in:
54
app/Services/Command/CreateContactCommand.php
Normal file
54
app/Services/Command/CreateContactCommand.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Command;
|
||||
|
||||
use App\Data\Command\BatchResult;
|
||||
use App\Data\Contact;
|
||||
use App\Data\Intent\Contact\CreateContactIntent;
|
||||
use App\Data\InvariantException;
|
||||
use App\Services\Mapper\ContactMapper;
|
||||
use App\Services\Storage\ContactStorageProvider;
|
||||
|
||||
class CreateContactCommand
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactMapper $contactMapper,
|
||||
private readonly ContactStorageProvider $contactStorageProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(CreateContactIntent $command): ?Contact
|
||||
{
|
||||
$entity = $this->contactMapper->tryToCreateFromIntent($command);
|
||||
|
||||
return $this->contactStorageProvider->create($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CreateContactIntent> $actualBatch
|
||||
* @return BatchResult
|
||||
*/
|
||||
public function multipleExecute(array $actualBatch): BatchResult
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
foreach ($actualBatch as $intent) {
|
||||
try {
|
||||
$entity = $this->contactMapper->tryToCreateFromIntent($intent);
|
||||
$contacts[] = $entity;
|
||||
} catch (InvariantException) {
|
||||
// Do nothing -> mark later as failed
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->contactStorageProvider->batchCreate($contacts);
|
||||
|
||||
return new BatchResult(
|
||||
count($actualBatch) - count($contacts),
|
||||
count($contacts) - $result,
|
||||
$result,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
app/Services/Command/DeleteContactCommand.php
Normal file
21
app/Services/Command/DeleteContactCommand.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Command;
|
||||
|
||||
use App\Data\Intent\Contact\DeleteContactIntent;
|
||||
use App\Services\Storage\ContactStorageProvider;
|
||||
|
||||
class DeleteContactCommand
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactStorageProvider $contactStorageProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(DeleteContactIntent $command): bool
|
||||
{
|
||||
return $this->contactStorageProvider->delete($command->uuid);
|
||||
}
|
||||
}
|
||||
25
app/Services/Command/ImportContactsCommand.php
Normal file
25
app/Services/Command/ImportContactsCommand.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Command;
|
||||
|
||||
use App\Data\Intent\ProcessImport\ImportContactsIntent;
|
||||
use App\Jobs\ProcessImport;
|
||||
use App\Services\ProcessImportMonitor\ProcessImportIdentifiable;
|
||||
use App\Services\ProcessImportMonitor\ProcessImportMonitor;
|
||||
|
||||
class ImportContactsCommand
|
||||
{
|
||||
public function __construct(private readonly ProcessImportMonitor $importMonitor)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(ImportContactsIntent $command): ProcessImportIdentifiable
|
||||
{
|
||||
$import = $this->importMonitor->newImport($command->path);
|
||||
ProcessImport::dispatch($command->path, $import);
|
||||
|
||||
return $import;
|
||||
}
|
||||
}
|
||||
26
app/Services/Command/UpdateContactCommand.php
Normal file
26
app/Services/Command/UpdateContactCommand.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Command;
|
||||
|
||||
use App\Data\Contact;
|
||||
use App\Data\Intent\Contact\UpdateContactIntent;
|
||||
use App\Services\Mapper\ContactMapper;
|
||||
use App\Services\Storage\ContactStorageProvider;
|
||||
|
||||
class UpdateContactCommand
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactMapper $contactMapper,
|
||||
private readonly ContactStorageProvider $contactStorageProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(UpdateContactIntent $command): ?Contact
|
||||
{
|
||||
$entity = $this->contactMapper->tryToCreateFromIntent($command);
|
||||
|
||||
return $this->contactStorageProvider->update($entity);
|
||||
}
|
||||
}
|
||||
25
app/Services/Mapper/Api/ContactImportMapper.php
Normal file
25
app/Services/Mapper/Api/ContactImportMapper.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Mapper\Api;
|
||||
|
||||
use App\Data\Api\ContactImport as ContactImportData;
|
||||
use App\Data\ContactImport;
|
||||
|
||||
final class ContactImportMapper
|
||||
{
|
||||
public function fromModel(ContactImport $contactImport): ContactImportData
|
||||
{
|
||||
return new ContactImportData(
|
||||
$contactImport->id,
|
||||
$contactImport->queueAt->toIso8601String(),
|
||||
$contactImport->startedAt?->toIso8601String(),
|
||||
$contactImport->finishedAt?->toIso8601String(),
|
||||
(int) $contactImport->totalProcessed,
|
||||
(int) $contactImport->errors,
|
||||
(int) $contactImport->duplicates,
|
||||
$contactImport->state,
|
||||
);
|
||||
}
|
||||
}
|
||||
44
app/Services/Mapper/ContactImportMapper.php
Normal file
44
app/Services/Mapper/ContactImportMapper.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Mapper;
|
||||
|
||||
use App\Data\ContactImport as ContactImportData;
|
||||
use App\Models\ContactImport;
|
||||
|
||||
final class ContactImportMapper
|
||||
{
|
||||
public function fromModel(ContactImport $contactImport): ContactImportData
|
||||
{
|
||||
return new ContactImportData(
|
||||
$contactImport->id,
|
||||
$contactImport->queue_at,
|
||||
$contactImport->started_at,
|
||||
$contactImport->finished_at,
|
||||
(int) $contactImport->total_processed,
|
||||
(int) $contactImport->errors,
|
||||
(int) $contactImport->duplicates,
|
||||
$contactImport->state->value,
|
||||
$contactImport->file,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toModelAttributes(ContactImportData $contactImport): array
|
||||
{
|
||||
return [
|
||||
'id' => $contactImport->id,
|
||||
'queue_at' => $contactImport->queueAt,
|
||||
'started_at' => $contactImport->startedAt,
|
||||
'finished_at' => $contactImport->finishedAt,
|
||||
'total_processed' => $contactImport->totalProcessed,
|
||||
'errors' => $contactImport->errors,
|
||||
'duplicates' => $contactImport->duplicates,
|
||||
'state' => $contactImport->state,
|
||||
'file' => $contactImport->file,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Services/Mapper/ContactMapper.php
Normal file
58
app/Services/Mapper/ContactMapper.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Mapper;
|
||||
|
||||
use App\Data\Contact as ContactData;
|
||||
use App\Data\Intent\Contact\CreateContactIntent;
|
||||
use App\Data\Intent\Contact\UpdateContactIntent;
|
||||
use App\Models\Contact;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final class ContactMapper
|
||||
{
|
||||
public function fromModel(Contact $contact): ContactData
|
||||
{
|
||||
return new ContactData(
|
||||
// @phpstan-ignore-next-line argument.type
|
||||
Uuid::fromString((string) $contact->id),
|
||||
// @phpstan-ignore-next-line argument.type
|
||||
$contact->email,
|
||||
// @phpstan-ignore-next-line argument.type
|
||||
$contact->first_name,
|
||||
// @phpstan-ignore-next-line argument.type
|
||||
$contact->last_name,
|
||||
);
|
||||
}
|
||||
|
||||
public function tryToCreateFromIntent(UpdateContactIntent|CreateContactIntent $intent): ContactData
|
||||
{
|
||||
if ($intent instanceof CreateContactIntent) {
|
||||
$uuid = Uuid::uuid7();
|
||||
} else {
|
||||
$uuid = $intent->uuid;
|
||||
}
|
||||
|
||||
return new ContactData(
|
||||
$uuid,
|
||||
$intent->email,
|
||||
$intent->firstName,
|
||||
$intent->lastName,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toModelAttributes(
|
||||
ContactData $contactData,
|
||||
): array {
|
||||
return [
|
||||
'id' => $contactData->uuid->toString(),
|
||||
'first_name' => $contactData->firstName,
|
||||
'last_name' => $contactData->lastName,
|
||||
'email' => $contactData->email,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProcessImportMonitor;
|
||||
|
||||
use Ramsey\Uuid\Rfc4122\UuidV7;
|
||||
|
||||
class DatabaseProcessImportMonitor implements ProcessImportMonitor
|
||||
{
|
||||
public function newImport(string $file): ProcessImportState&ProcessImportIdentifiable
|
||||
{
|
||||
return new DatabaseProcessImportState(UuidV7::uuid7(), $file);
|
||||
}
|
||||
}
|
||||
115
app/Services/ProcessImportMonitor/DatabaseProcessImportState.php
Normal file
115
app/Services/ProcessImportMonitor/DatabaseProcessImportState.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProcessImportMonitor;
|
||||
|
||||
use App\Enums\ContactImportStateEnum;
|
||||
use App\Models\ContactImport;
|
||||
use Carbon\Carbon;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class DatabaseProcessImportState implements ProcessImportIdentifiable, ProcessImportState
|
||||
{
|
||||
public function __construct(
|
||||
public readonly UuidInterface $uuid,
|
||||
string $file
|
||||
) {
|
||||
ContactImport::query()->create(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
'state' => ContactImportStateEnum::Pending,
|
||||
'file' => $file,
|
||||
'queue_at' => Carbon::now(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function uuid(): UuidInterface
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
]
|
||||
)->update(
|
||||
[
|
||||
'started_at' => now(),
|
||||
'state' => ContactImportStateEnum::Running,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function fail(): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
],
|
||||
)->update(
|
||||
[
|
||||
'finished_at' => now(),
|
||||
'state' => ContactImportStateEnum::Fail,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function finish(): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
],
|
||||
)->update(
|
||||
[
|
||||
'finished_at' => now(),
|
||||
'state' => ContactImportStateEnum::Done,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function contactDuplicate(int $processed = 1): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
],
|
||||
)->incrementEach(
|
||||
[
|
||||
'total_processed' => $processed,
|
||||
'duplicates' => $processed,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function contactFail(int $processed = 1): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
],
|
||||
)->incrementEach(
|
||||
[
|
||||
'total_processed' => $processed,
|
||||
'errors' => $processed,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function contactSuccess(int $processed = 1): void
|
||||
{
|
||||
ContactImport::query()->where(
|
||||
[
|
||||
'id' => $this->uuid()->toString(),
|
||||
],
|
||||
)->incrementEach(
|
||||
[
|
||||
'total_processed' => $processed,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProcessImportMonitor;
|
||||
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
interface ProcessImportIdentifiable
|
||||
{
|
||||
public UuidInterface $uuid { get; }
|
||||
}
|
||||
10
app/Services/ProcessImportMonitor/ProcessImportMonitor.php
Normal file
10
app/Services/ProcessImportMonitor/ProcessImportMonitor.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProcessImportMonitor;
|
||||
|
||||
interface ProcessImportMonitor
|
||||
{
|
||||
public function newImport(string $file): ProcessImportState&ProcessImportIdentifiable;
|
||||
}
|
||||
29
app/Services/ProcessImportMonitor/ProcessImportState.php
Normal file
29
app/Services/ProcessImportMonitor/ProcessImportState.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\ProcessImportMonitor;
|
||||
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
/**
|
||||
* Handling actual state of import
|
||||
*
|
||||
* Never use not-serializable values in implementations!
|
||||
*/
|
||||
interface ProcessImportState
|
||||
{
|
||||
public function uuid(): UuidInterface;
|
||||
|
||||
public function start(): void;
|
||||
|
||||
public function finish(): void;
|
||||
|
||||
public function fail(): void;
|
||||
|
||||
public function contactFail(int $processed = 1): void;
|
||||
|
||||
public function contactDuplicate(int $processed = 1): void;
|
||||
|
||||
public function contactSuccess(int $processed = 1): void;
|
||||
}
|
||||
29
app/Services/Query/ImportContactsStateQuery.php
Normal file
29
app/Services/Query/ImportContactsStateQuery.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Query;
|
||||
|
||||
use App\Data\ContactImport;
|
||||
use App\Data\Intent\ProcessImport\SearchImportContactsIntent;
|
||||
use App\Services\Mapper\ContactImportMapper;
|
||||
use App\Services\Storage\ProcessImportStorageProvider;
|
||||
|
||||
class ImportContactsStateQuery
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactImportMapper $contactImportMapper,
|
||||
private readonly ProcessImportStorageProvider $processImportStorageProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function executeOne(SearchImportContactsIntent $search): ?ContactImport
|
||||
{
|
||||
$modelResult = $this->processImportStorageProvider->fetchByUuid($search->uuid);
|
||||
if ($modelResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->contactImportMapper->fromModel($modelResult);
|
||||
}
|
||||
}
|
||||
59
app/Services/Query/SearchContactQuery.php
Normal file
59
app/Services/Query/SearchContactQuery.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Query;
|
||||
|
||||
use App\Data\Intent\Contact\SearchContactIntent;
|
||||
use App\Data\Intent\Contact\SearchContactIntentByUuid;
|
||||
use App\Models\Contact;
|
||||
use App\Services\Mapper\ContactMapper;
|
||||
use App\Services\SearchProvider\SearchProvider;
|
||||
use App\Services\Storage\ContactStorageProvider;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class SearchContactQuery
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SearchProvider $searchProvider,
|
||||
private readonly ContactStorageProvider $contactStorageProvider,
|
||||
private readonly ContactMapper $contactMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function executeOne(SearchContactIntentByUuid $search): ?\App\Data\Contact
|
||||
{
|
||||
$modelResult = $this->contactStorageProvider->fetchByUuid($search->uuid);
|
||||
if ($modelResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->contactMapper->fromModel($modelResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, \App\Data\Contact>
|
||||
*/
|
||||
public function execute(SearchContactIntent $searchIntent): LengthAwarePaginator
|
||||
{
|
||||
if ($searchIntent->query == SearchContactIntent::queryAll()->query) {
|
||||
/** @var LengthAwarePaginator<int, Contact> $modelResult */
|
||||
$modelResult = $this->contactStorageProvider->fetchPaginatedAll($searchIntent->resultsPerPage);
|
||||
} else {
|
||||
/** @var LengthAwarePaginator<int, Contact> $modelResult */
|
||||
$modelResult = $this->searchProvider->search($searchIntent->query, $searchIntent->resultsPerPage);
|
||||
}
|
||||
|
||||
$collection = $modelResult->getCollection()->map(
|
||||
fn (Contact $contact) => $this->contactMapper->fromModel($contact),
|
||||
);
|
||||
|
||||
return new LengthAwarePaginator(
|
||||
$collection,
|
||||
$modelResult->total(),
|
||||
$modelResult->perPage(),
|
||||
$modelResult->currentPage(),
|
||||
$modelResult->getOptions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
app/Services/SearchProvider/DatabaseSearchProvider.php
Normal file
34
app/Services/SearchProvider/DatabaseSearchProvider.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\SearchProvider;
|
||||
|
||||
use App\Models\Contact;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
final class DatabaseSearchProvider implements SearchProvider
|
||||
{
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, \App\Models\Contact>
|
||||
*/
|
||||
public function search(string $query, int $paginate): LengthAwarePaginator
|
||||
{
|
||||
return Contact::query()
|
||||
->select('*')
|
||||
->selectRaw(
|
||||
"CASE
|
||||
WHEN email LIKE CONCAT('%', ?::text, '%')
|
||||
THEN 1.0
|
||||
ELSE ts_rank(ts_name, plainto_tsquery('simple', ?::text))
|
||||
END AS rank",
|
||||
[$query, $query]
|
||||
)
|
||||
->whereRaw(
|
||||
"ts_name @@ plainto_tsquery('simple', ?::text) OR email LIKE CONCAT('%', ?::text, '%')",
|
||||
[$query, $query]
|
||||
)
|
||||
->orderByDesc('rank')
|
||||
->paginate($paginate);
|
||||
}
|
||||
}
|
||||
16
app/Services/SearchProvider/SearchProvider.php
Normal file
16
app/Services/SearchProvider/SearchProvider.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\SearchProvider;
|
||||
|
||||
use App\Data\Contact;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface SearchProvider
|
||||
{
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, \App\Models\Contact>
|
||||
*/
|
||||
public function search(string $query, int $paginate): LengthAwarePaginator;
|
||||
}
|
||||
30
app/Services/Storage/ContactStorageProvider.php
Normal file
30
app/Services/Storage/ContactStorageProvider.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Storage;
|
||||
|
||||
use App\Data\Contact;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
interface ContactStorageProvider
|
||||
{
|
||||
public function create(Contact $contact): ?Contact;
|
||||
|
||||
public function update(Contact $contact): ?Contact;
|
||||
|
||||
public function delete(UuidInterface $uuid): bool;
|
||||
|
||||
/**
|
||||
* @param array<Contact> $intents
|
||||
*/
|
||||
public function batchCreate(array $intents): int;
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, \App\Models\Contact>
|
||||
*/
|
||||
public function fetchPaginatedAll(int $paginate): LengthAwarePaginator;
|
||||
|
||||
public function fetchByUuid(UuidInterface $uuid): ?\App\Models\Contact;
|
||||
}
|
||||
72
app/Services/Storage/DatabaseContactStorageProvider.php
Normal file
72
app/Services/Storage/DatabaseContactStorageProvider.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Storage;
|
||||
|
||||
use App\Data\Contact;
|
||||
use App\Services\Mapper\ContactMapper;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class DatabaseContactStorageProvider implements ContactStorageProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactMapper $contactMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(Contact $contact): ?Contact
|
||||
{
|
||||
$contact = \App\Models\Contact::query()->create(
|
||||
$this->contactMapper->toModelAttributes($contact),
|
||||
);
|
||||
|
||||
return $this->contactMapper->fromModel($contact);
|
||||
}
|
||||
|
||||
public function update(Contact $contact): ?Contact
|
||||
{
|
||||
$existingContact = \App\Models\Contact::query()->findOrFail($contact->uuid->toString());
|
||||
$result = $existingContact->update($this->contactMapper->toModelAttributes($contact));
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->contactMapper->fromModel($existingContact);
|
||||
}
|
||||
|
||||
public function delete(UuidInterface $uuid): bool
|
||||
{
|
||||
$contact = \App\Models\Contact::query()->findOrFail($uuid->toString());
|
||||
|
||||
return (bool) $contact->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Contact> $intents
|
||||
*/
|
||||
public function batchCreate(array $intents): int
|
||||
{
|
||||
return \App\Models\Contact::query()->insertOrIgnore(
|
||||
array_map(function (Contact $intent): array {
|
||||
return $this->contactMapper->toModelAttributes($intent);
|
||||
}, $intents)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, \App\Models\Contact>
|
||||
*/
|
||||
public function fetchPaginatedAll(int $paginate): LengthAwarePaginator
|
||||
{
|
||||
return \App\Models\Contact::query()
|
||||
->orderBy('id')
|
||||
->paginate($paginate);
|
||||
}
|
||||
|
||||
public function fetchByUuid(UuidInterface $uuid): ?\App\Models\Contact
|
||||
{
|
||||
return \App\Models\Contact::query()->find($uuid->toString());
|
||||
}
|
||||
}
|
||||
15
app/Services/Storage/DatabaseImportStorageProvider.php
Normal file
15
app/Services/Storage/DatabaseImportStorageProvider.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Storage;
|
||||
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class DatabaseImportStorageProvider implements ProcessImportStorageProvider
|
||||
{
|
||||
public function fetchByUuid(UuidInterface $uuid): ?\App\Models\ContactImport
|
||||
{
|
||||
return \App\Models\ContactImport::query()->find($uuid->toString());
|
||||
}
|
||||
}
|
||||
13
app/Services/Storage/ProcessImportStorageProvider.php
Normal file
13
app/Services/Storage/ProcessImportStorageProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Storage;
|
||||
|
||||
use App\Models\ContactImport;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
interface ProcessImportStorageProvider
|
||||
{
|
||||
public function fetchByUuid(UuidInterface $uuid): ?ContactImport;
|
||||
}
|
||||
83
app/Services/Validators/ContactValidator.php
Normal file
83
app/Services/Validators/ContactValidator.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Validators;
|
||||
|
||||
use App\Data\Contact;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class ContactValidator
|
||||
{
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email:rfc', 'max:254'],
|
||||
'first_name' => ['nullable', 'string', 'max:100'],
|
||||
'last_name' => ['nullable', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rulesForStore(): array
|
||||
{
|
||||
return $this->rulesForRequest(unique: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rulesForUpdate(Contact $contact): array
|
||||
{
|
||||
return $this->rulesForRequest(unique: true, ignore: $contact);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'first_name.max' => 'First name may not be greater than 100 characters.',
|
||||
'last_name.max' => 'Last name may not be greater than 100 characters.',
|
||||
'email.required' => 'Email is required.',
|
||||
'email.email' => 'Email must be a valid email address.',
|
||||
'email.max' => 'Email may not be greater than 254 characters.',
|
||||
'email.unique' => 'Email has already been taken.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
private function rulesForRequest(bool $unique, ?Contact $ignore = null): array
|
||||
{
|
||||
$rules = $this->rules();
|
||||
/**
|
||||
* @var array<string> $emailRules
|
||||
*/
|
||||
$emailRules = Arr::pull($rules, 'email');
|
||||
|
||||
if ($unique) {
|
||||
$rule = Rule::unique('contacts', 'email');
|
||||
if ($ignore !== null) {
|
||||
$rule = $rule->ignore($ignore->uuid->toString());
|
||||
}
|
||||
$emailRules[] = $rule;
|
||||
}
|
||||
|
||||
/** @var array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> $result */
|
||||
$result = [
|
||||
...$rules,
|
||||
'email' => $emailRules,
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user