feat: initial project setup with Laravel 12 and Filament v4
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class PdfService
|
||||
{
|
||||
|
||||
public static function download(string $view, array $data, string $filename): StreamedResponse
|
||||
{
|
||||
if (class_exists(\Mpdf\Mpdf::class)) {
|
||||
return self::downloadWithMpdf($view, $data, $filename);
|
||||
}
|
||||
|
||||
return self::downloadWithDompdf($view, $data, $filename);
|
||||
}
|
||||
|
||||
private static function downloadWithMpdf(string $view, array $data, string $filename): StreamedResponse
|
||||
{
|
||||
$defaultConfig = (new \Mpdf\Config\ConfigVariables())->getDefaults();
|
||||
$defaultFontConfig = (new \Mpdf\Config\FontVariables())->getDefaults();
|
||||
|
||||
$fontsDir = storage_path('fonts');
|
||||
|
||||
$mpdf = new \Mpdf\Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'orientation' => 'P',
|
||||
'margin_top' => 15,
|
||||
'margin_bottom' => 15,
|
||||
'margin_left' => 15,
|
||||
'margin_right' => 15,
|
||||
'tempDir' => sys_get_temp_dir(),
|
||||
'fontDir' => array_merge($defaultConfig['fontDir'], [$fontsDir]),
|
||||
'fontdata' => $defaultFontConfig['fontdata'] + [
|
||||
'tahoma' => [
|
||||
'R' => 'tahoma.ttf',
|
||||
'B' => 'tahomabd.ttf',
|
||||
'useOTL' => 0xFF,
|
||||
'useKashida' => 75,
|
||||
],
|
||||
],
|
||||
'default_font' => 'tahoma',
|
||||
]);
|
||||
|
||||
$mpdf->autoScriptToLang = true;
|
||||
$mpdf->baseScript = 1;
|
||||
$mpdf->autoArabic = true;
|
||||
$mpdf->autoLangToFont = true;
|
||||
|
||||
$html = view($view, $data)->render();
|
||||
$mpdf->WriteHTML($html);
|
||||
$output = $mpdf->Output('', 'S');
|
||||
|
||||
return response()->streamDownload(
|
||||
fn () => print($output),
|
||||
$filename,
|
||||
['Content-Type' => 'application/pdf'],
|
||||
);
|
||||
}
|
||||
|
||||
private static function downloadWithDompdf(string $view, array $data, string $filename): StreamedResponse
|
||||
{
|
||||
$pdf = Pdf::loadView($view, $data)->setPaper('a4', 'portrait');
|
||||
|
||||
return response()->streamDownload(
|
||||
fn () => print($pdf->output()),
|
||||
$filename,
|
||||
['Content-Type' => 'application/pdf'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\OsurgInitial;
|
||||
use App\Models\SyncLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SyncService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $peerIp,
|
||||
private readonly int $peerPort,
|
||||
private readonly string $token,
|
||||
private readonly string $ftpUser,
|
||||
private readonly string $ftpPass,
|
||||
private readonly int $ftpPort,
|
||||
private readonly string $ftpRemotePath,
|
||||
) {}
|
||||
|
||||
public static function fromConfig(): static
|
||||
{
|
||||
return new static(
|
||||
peerIp: OsurgInitial::val('sync_peer_ip', config('sync.peer_ip', '')),
|
||||
peerPort: (int) OsurgInitial::val('sync_peer_port', config('sync.peer_port', 80)),
|
||||
token: OsurgInitial::val('sync_token', config('sync.token', '')),
|
||||
ftpUser: OsurgInitial::val('ftp_user', config('sync.ftp_user', '')),
|
||||
ftpPass: OsurgInitial::val('ftp_pass', config('sync.ftp_pass', '')),
|
||||
ftpPort: (int) OsurgInitial::val('ftp_port', config('sync.ftp_port', 21)),
|
||||
ftpRemotePath: OsurgInitial::val('ftp_remote_path', config('sync.ftp_remote_path', '')),
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->peerIp !== '' && $this->token !== '';
|
||||
}
|
||||
|
||||
public function isFtpConfigured(): bool
|
||||
{
|
||||
return $this->peerIp !== '' && $this->ftpUser !== '' && $this->ftpRemotePath !== '';
|
||||
}
|
||||
|
||||
public function sync(): array
|
||||
{
|
||||
$report = [];
|
||||
$baseUrl = "http://{$this->peerIp}:{$this->peerPort}";
|
||||
|
||||
$ourLastSync = SyncLog::lastSyncId();
|
||||
|
||||
try {
|
||||
$pullResponse = Http::timeout(30)
|
||||
->withHeader('X-Sync-Token', $this->token)
|
||||
->get("{$baseUrl}/api/sync/export", ['since' => $ourLastSync]);
|
||||
|
||||
if ($pullResponse->successful()) {
|
||||
$peerChanges = $pullResponse->json('changes', []);
|
||||
|
||||
if (! empty($peerChanges)) {
|
||||
app()->instance('sync.applying', true);
|
||||
|
||||
DB::transaction(function () use ($peerChanges, &$report) {
|
||||
foreach ($peerChanges as $change) {
|
||||
$this->applyChange($change, 'دریافت', $report);
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->isFtpConfigured()) {
|
||||
$this->transferFilesViaFtp($peerChanges, 's2c', $report);
|
||||
} else {
|
||||
$this->syncFilesViaHttp($peerChanges, $baseUrl, $report);
|
||||
}
|
||||
}
|
||||
|
||||
$report[] = count($peerChanges) . ' تغییر از سیستم مقابل دریافت شد.';
|
||||
} else {
|
||||
$report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$report[] = 'خطا در اتصال به سیستم مقابل: ' . $e->getMessage();
|
||||
return ['success' => false, 'report' => $report];
|
||||
}
|
||||
|
||||
$ourChanges = SyncLog::changesSince($ourLastSync)->toArray();
|
||||
|
||||
try {
|
||||
$pushResponse = Http::timeout(120)
|
||||
->withHeader('X-Sync-Token', $this->token)
|
||||
->post("{$baseUrl}/api/sync/apply", ['changes' => $ourChanges]);
|
||||
|
||||
if ($pushResponse->successful()) {
|
||||
foreach ($pushResponse->json('report', []) as $line) {
|
||||
$report[] = 'ارسال: ' . $line;
|
||||
}
|
||||
$report[] = count($ourChanges) . ' تغییر به سیستم مقابل ارسال شد.';
|
||||
|
||||
if ($this->isFtpConfigured()) {
|
||||
$this->transferFilesViaFtp($ourChanges, 'c2s', $report);
|
||||
} else {
|
||||
$this->pushFilesViaHttp($ourChanges, $baseUrl, $report);
|
||||
}
|
||||
} else {
|
||||
$report[] = 'خطا در ارسال تغییرات به سیستم مقابل: ' . $pushResponse->status();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$report[] = 'خطا در ارسال به سیستم مقابل: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
SyncLog::markSynced(request()->ip() ?? '127.0.0.1');
|
||||
|
||||
return ['success' => true, 'report' => $report];
|
||||
}
|
||||
|
||||
public function applyChanges(array $changes): array
|
||||
{
|
||||
$report = [];
|
||||
app()->instance('sync.applying', true);
|
||||
|
||||
DB::transaction(function () use ($changes, &$report) {
|
||||
foreach ($changes as $change) {
|
||||
$this->applyChange($change, '', $report);
|
||||
}
|
||||
});
|
||||
|
||||
SyncLog::markSynced(request()->ip() ?? '');
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
private function transferFilesViaFtp(array $changes, string $side, array &$report): void
|
||||
{
|
||||
$ftp = $this->connectFtp();
|
||||
if (! $ftp) {
|
||||
$report[] = 'خطا در اتصال FTP به سیستم مقابل';
|
||||
return;
|
||||
}
|
||||
|
||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
|
||||
$remoteBase = rtrim($this->ftpRemotePath, '/\\');
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$data = $change['changed_data'] ?? [];
|
||||
foreach ($fileFields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||
foreach ($files as $filePath) {
|
||||
if (! $filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = ltrim(str_replace('\\', '/', $filePath), '/');
|
||||
$localFile = $localBase . '/' . $filePath;
|
||||
$remoteFile = $remoteBase . '/' . $filePath;
|
||||
$remoteFolder = dirname($remoteFile);
|
||||
|
||||
if ($side === 'c2s') {
|
||||
if (! file_exists($localFile)) {
|
||||
continue;
|
||||
}
|
||||
@ftp_mkdir($ftp, $remoteFolder);
|
||||
if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) {
|
||||
$report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓";
|
||||
} else {
|
||||
$report[] = "فایل [{$filePath}] ارسال نشد (FTP) ✗";
|
||||
}
|
||||
} else {
|
||||
if (file_exists($localFile)) {
|
||||
continue;
|
||||
}
|
||||
$localFolder = dirname($localFile);
|
||||
if (! is_dir($localFolder)) {
|
||||
mkdir($localFolder, 0755, true);
|
||||
}
|
||||
if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) {
|
||||
$report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓";
|
||||
} else {
|
||||
$report[] = "فایل [{$filePath}] دریافت نشد (FTP) ✗";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ftp_close($ftp);
|
||||
}
|
||||
|
||||
private function connectFtp(): mixed
|
||||
{
|
||||
if (! function_exists('ftp_connect')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ftp = @ftp_connect($this->peerIp, $this->ftpPort, 30);
|
||||
if (! $ftp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! @ftp_login($ftp, $this->ftpUser, $this->ftpPass)) {
|
||||
ftp_close($ftp);
|
||||
return null;
|
||||
}
|
||||
|
||||
ftp_pasv($ftp, true);
|
||||
|
||||
return $ftp;
|
||||
}
|
||||
|
||||
private function pushFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
||||
{
|
||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$data = $change['changed_data'] ?? [];
|
||||
foreach ($fileFields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||
foreach ($files as $filePath) {
|
||||
if (! $filePath || ! Storage::disk('public')->exists($filePath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$response = Http::timeout(600)
|
||||
->withHeader('X-Sync-Token', $this->token)
|
||||
->attach('file', Storage::disk('public')->get($filePath), basename($filePath))
|
||||
->post("{$baseUrl}/api/sync/receive-file", ['path' => $filePath]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓";
|
||||
} else {
|
||||
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function syncFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
||||
{
|
||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$data = $change['changed_data'] ?? [];
|
||||
foreach ($fileFields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||
foreach ($files as $filePath) {
|
||||
if (! $filePath || Storage::disk('public')->exists($filePath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$response = Http::timeout(60)
|
||||
->withHeader('X-Sync-Token', $this->token)
|
||||
->get("{$baseUrl}/api/sync/file", ['path' => $filePath]);
|
||||
|
||||
if ($response->successful()) {
|
||||
Storage::disk('public')->put($filePath, $response->body());
|
||||
$report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyChange(array $change, string $prefix, array &$report): void
|
||||
{
|
||||
$table = $change['table_name'];
|
||||
$recordId = $change['record_id'];
|
||||
$action = $change['action'];
|
||||
$data = $change['changed_data'] ?? [];
|
||||
|
||||
$label = $prefix ? "{$prefix} [{$table}#{$recordId}]" : "[{$table}#{$recordId}]";
|
||||
|
||||
try {
|
||||
if ($action === 'deleted') {
|
||||
DB::table($table)->where('id', $recordId)->delete();
|
||||
$report[] = "{$label} حذف ✓";
|
||||
} elseif ($action === 'created') {
|
||||
$exists = DB::table($table)->where('id', $recordId)->exists();
|
||||
if ($exists) {
|
||||
DB::table($table)->where('id', $recordId)->update($this->sanitize($data));
|
||||
$report[] = "{$label} ایجاد→بروزرسانی ✓";
|
||||
} else {
|
||||
DB::table($table)->insert($this->sanitize(array_merge(['id' => $recordId], $data)));
|
||||
$report[] = "{$label} ایجاد ✓";
|
||||
}
|
||||
} elseif ($action === 'updated') {
|
||||
DB::table($table)->where('id', $recordId)->update($this->sanitize($data));
|
||||
$report[] = "{$label} بروزرسانی ✓";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$report[] = "{$label} {$action} ✗ " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitize(array $data): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$result[$key] = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
} elseif (is_scalar($value) || $value === null) {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user