This commit is contained in:
f4ilji
2025-01-30 00:31:40 +05:00
parent bad611eb84
commit 3385dc5807
60 changed files with 1811 additions and 825 deletions
@@ -0,0 +1,187 @@
<?php
namespace App\Services\Filament\Domain\Posts;
use App\Enums\PostStatus;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class PostDataProcessor
{
/**
* Обрабатывает данные перед созданием поста.
*
* @param array $data
* @return array
*/
public function process(array $data): array
{
// Удаляем ненужные данные
unset($data['publication']);
// Устанавливаем текст для предпросмотра
$data['preview_text'] = $this->setPreviewText($data);
// Устанавливаем время публикации
$data['publish_at'] = $this->setPublishDateTime($data['publish_setting'], $data['status']);
unset($data['publish_setting']);
// Генерируем данные для поиска
$data['search_data'] = $this->generateSearchData($data['content']);
// Рассчитываем время чтения
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
// Устанавливаем ID текущего пользователя
$data['user_id'] = auth()->id();
return $data;
}
/**
* Устанавливает текст для предпросмотра.
*
* @param array $data
* @return string
*/
private function setPreviewText(array $data): string
{
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
if ($rowData === null) {
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
}
$previewText = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : '';
return Str::limit($previewText, 160);
}
/**
* Устанавливает время публикации.
*
* @param array $publishSetting
* @param string $status
* @return Carbon|null
*/
private function setPublishDateTime(array $publishSetting, string|PostStatus $status): ?Carbon
{
if ($publishSetting['publish_after'] === true) {
return Carbon::parse($publishSetting['publish_at']);
}
if ($status === PostStatus::PUBLISHED) {
return Carbon::now();
}
return null;
}
/**
* Генерирует данные для поиска.
*
* @param array $content
* @return string
*/
private function generateSearchData(array $content): string
{
$result = "";
foreach ($content as $block) {
$result .= $this->getDataFromBlocks($block);
}
// Удаляем лишние пробелы и переносы строк
$result = preg_replace('/\s+/', ' ', $result);
$result = trim($result);
return strtolower($result);
}
/**
* Извлекает данные из блоков контента.
*
* @param array $block
* @return string
*/
private function getDataFromBlocks(array $block): string
{
$data = "";
switch ($block['type']) {
case 'paragraph':
$data .= strip_tags($block['data']['content']) . " ";
break;
case 'heading':
$data .= strip_tags($block['data']['content']) . " ";
break;
case 'files':
foreach ($block['data']['file'] as $file) {
$data .= $file['title'] . " ";
}
break;
case 'person':
$data .= $block['data']['name'] . " ";
break;
case 'stepper':
$data .= $block['data']['step_name'] . " ";
foreach ($block['data']['steps'] as $step) {
$data .= $step['title'] . " ";
$data .= strip_tags($step['content']) . " ";
}
break;
case 'tabs':
foreach ($block['data']['tab'] as $item) {
foreach ($item['content'] as $block) {
$data .= $this->getDataFromBlocks($block);
}
}
break;
}
return $data;
}
/**
* Рассчитывает время чтения.
*
* @param string $text
* @return int
*/
private function calculateReadingTime(string $text): int
{
$wordCount = str_word_count($text, 0, "АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя");
$wordsPerMinute = 120; // Средняя скорость чтения
return max(1, round($wordCount / $wordsPerMinute));
}
/**
* Находит первый блок по имени.
*
* @param string $name
* @param array $content
* @return array|null
*/
private function getFirstBlockByName(string $name, array $content): ?array
{
foreach ($content as $block) {
if ($block['type'] === $name) {
return $block;
}
}
return null;
}
/**
* Находит блок по SEO-активности.
*
* @param string $name
* @param array $content
* @return array|null
*/
private function getBlockBySeoActiveState(string $name, array $content): ?array
{
foreach ($content as $block) {
if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) {
return $block;
}
}
return null;
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Services\Filament\Domain\Posts;
use App\Filament\Resources\PostResource;
use App\Models\Post;
use App\Models\Role;
use App\Models\User;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
class PostNotificationService
{
/**
* Отправляет уведомления редакторам о новом посте.
*
* @param Post $post
* @return void
*/
public function send(Post $post): void
{
// Находим всех пользователей с ролью "editor"
$editors = Role::findByName('editor')->users;
// Отправляем уведомление каждому редактору
foreach ($editors as $editor) {
Notification::make()
->title('Новость на проверку')
->body('Новость "' . $post->title . '" нуждается в проверке')
->actions([
Action::make('view')
->label('Проверить')
->button()
->markAsRead()
->url(PostResource::getUrl('edit', ['record' => $post])),
])
->sendToDatabase($editor); // Отправляем уведомление конкретному редактору
}
}
public function sendSuccessNotification(Post $post): void
{
$user = User::find($post->user_id);
Notification::make()
->title('Ваша новость опубликована')
->body('Новость "' . $post->title . '" опубликована')
->actions([
Action::make('view')
->label('Смотреть')
->button()
->markAsRead()
->url(route('client.post.show', $post->slug)),
])
->sendToDatabase($user);
}
/**
* Отправляет уведомление об отклонении.
*
* @param Post $post
* @return void
*/
public function sendDeniedNotification(Post $post): void
{
$user = User::find($post->user_id);
Notification::make()
->title('Ваша новость отклонена :(')
->body('Новость "' . $post->title . '" была отклонена')
->actions([
Action::make('view')
->label('Смотреть')
->button()
->markAsRead()
->url(PostResource::getUrl('edit', ['record' => $post])),
])
->sendToDatabase($user);
}
}
@@ -0,0 +1,111 @@
<?php
namespace App\Services\Filament\Domain\Posts;
use Illuminate\Support\Str;
class PostSeoGenerator
{
/**
* Генерирует SEO-данные для поста.
*
* @param array $data
* @return array
*/
public function generate(array $data): array
{
return [
'title' => $this->extractSeoTitle($data),
'description' => $this->extractSeoDescription($data['content']),
'image' => $this->extractSeoImage($data),
];
}
/**
* Извлекает SEO-заголовок.
*
* @param array $data
* @return string
*/
private function extractSeoTitle(array $data): string
{
return $data['title'];
}
/**
* Извлекает SEO-описание.
*
* @param array $content
* @return string
*/
private function extractSeoDescription(array $content): string
{
$rowData = $this->getBlockBySeoActiveState('paragraph', $content);
if ($rowData === null) {
$rowData = $this->getFirstBlockByName('paragraph', $content);
}
$description = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : '';
return Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160);
}
/**
* Извлекает SEO-изображение.
*
* @param array $data
* @return string|null
*/
private function extractSeoImage(array $data): ?string
{
return $data['preview'] ?? null;
}
/**
* Находит первый блок по имени.
*
* @param string $name
* @param array $content
* @return array|null
*/
private function getFirstBlockByName(string $name, array $content): ?array
{
foreach ($content as $block) {
if ($block['type'] === $name) {
return $block;
}
}
return null;
}
/**
* Находит блок по SEO-активности.
*
* @param string $name
* @param array $content
* @return array|null
*/
private function getBlockBySeoActiveState(string $name, array $content): ?array
{
foreach ($content as $block) {
if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) {
return $block;
}
}
return null;
}
public function setPreviewText(array $data): ?string
{
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
if ($rowData === null) {
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
}
if ($rowData !== null) {
$previewText = html_entity_decode(strip_tags($rowData['data']['content']));
return Str::limit($previewText, 160);
}
return null;
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Services\Filament\Domain\Posts;
use App\Dto\MainSliderDTO;
use App\Models\MainSlider;
use Illuminate\Support\Facades\Log;
class PostSliderService
{
public function __construct(
readonly private MainSliderDTO $dto,
readonly private string $postSlug,
){}
public function create(): void
{
try {
MainSlider::create([
'title' => $this->dto->title,
'content' => $this->dto->content,
'image' => $this->dto->image,
'link' => parse_url(route('client.post.show', $this->postSlug), PHP_URL_PATH),
'link_text' => $this->dto->link_text,
'is_active' => $this->dto->is_active,
'color_theme' => $this->dto->color_theme,
'start_time' => $this->dto->start_time,
'end_time' => $this->dto->end_time,
]);
Log::info('MainSlider created successfully', ['postSlug' => $this->postSlug]);
} catch (\Exception $e) {
Log::error('Failed to create MainSlider', ['error' => $e->getMessage()]);
throw $e;
}
}
public function update(): void
{
try {
MainSlider::update([
'title' => $this->dto->title,
'content' => $this->dto->content,
'image' => $this->dto->image,
'link' => parse_url(route('client.post.show', $this->postSlug), PHP_URL_PATH),
'link_text' => $this->dto->link_text,
'is_active' => $this->dto->is_active,
'color_theme' => $this->dto->color_theme,
'start_time' => $this->dto->start_time,
'end_time' => $this->dto->end_time,
]);
Log::info('MainSlider updated successfully', ['postSlug' => $this->postSlug]);
} catch (\Exception $e) {
Log::error('Failed to update MainSlider', ['error' => $e->getMessage()]);
throw $e;
}
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Services\Filament\Domain\Posts;
use App\Enums\PostStatus;
use App\Jobs\CreateVkPost;
use App\Models\Post;
use Illuminate\Support\Facades\Storage;
class VkPostPublisher
{
/**
* Публикует пост в социальных сетях.
*
* @param array $settings
* @param Post $post
* @return void
*/
public function publish(array $settings, Post $post): void
{
if ($post->status === PostStatus::PUBLISHED) {
if ($settings['vk']) {
$text = $this->generateContentForVk($post->content);
$images = $this->generateImageLinksForVk($post->images);
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
dispatch(new CreateVkPost($post->title, $text, $images, $post->id, $publishDate));
}
}
}
/**
* Генерирует текстовый контент для ВКонтакте.
*
* @param array $content
* @return string
*/
private function generateContentForVk(array $content): string
{
$text = '';
foreach ($content as $block) {
switch ($block['type']) {
case 'paragraph':
$text .= strip_tags($block['data']['content']) . "\n\n";
break;
case 'heading':
$text .= strip_tags($block['data']['content']) . "\n\n";
break;
}
}
return trim($text);
}
/**
* Генерирует ссылки на изображения для ВКонтакте.
*
* @param array $images
* @return array
*/
private function generateImageLinksForVk(array $images): array
{
return array_map(function ($file) {
return Storage::url($file); // Генерируем полный URL для изображения
}, $images);
}
}