refactor: use DTO and terminate middleware for better performance
- Add PageviewData readonly DTO class for type safety - Update AnalyticsClient to accept PageviewData instead of array - Move analytics sending to terminate() method (runs after response sent) - Update all tests for new DTO pattern
This commit is contained in:
6 files changed
+154
-116
No files matched your search
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mattitja\Analytics\Data;
|
||||
|
||||
readonly class PageviewData
|
||||
{
|
||||
public function __construct(
|
||||
public string $siteKey,
|
||||
public string $url,
|
||||
public string $ip,
|
||||
public string $userAgent,
|
||||
public ?string $referrer = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = [
|
||||
'k' => $this->siteKey,
|
||||
'u' => $this->url,
|
||||
'ip' => $this->ip,
|
||||
'ua' => $this->userAgent,
|
||||
];
|
||||
|
||||
if ($this->referrer !== null && $this->referrer !== '') {
|
||||
$data['r'] = $this->referrer;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ namespace Mattitja\Analytics\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mattitja\Analytics\Data\PageviewData;
|
||||
use Mattitja\Analytics\Services\AnalyticsClient;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AnalyticsMiddleware
|
||||
{
|
||||
@@ -26,6 +27,8 @@ class AnalyticsMiddleware
|
||||
'twitterbot',
|
||||
];
|
||||
|
||||
private ?PageviewData $pendingData = null;
|
||||
|
||||
/**
|
||||
* Handle an incoming request and capture analytics data.
|
||||
*/
|
||||
@@ -34,13 +37,22 @@ class AnalyticsMiddleware
|
||||
$response = $next($request);
|
||||
|
||||
if ($this->shouldCaptureAnalytics($request)) {
|
||||
$data = $this->buildAnalyticsData($request);
|
||||
$this->sendAnalytics($data);
|
||||
$this->pendingData = $this->buildAnalyticsData($request);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tasks after the response has been sent to the browser.
|
||||
*/
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
if ($this->pendingData !== null) {
|
||||
$this->sendAnalytics($this->pendingData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if analytics should be captured for this request.
|
||||
*/
|
||||
@@ -136,34 +148,27 @@ class AnalyticsMiddleware
|
||||
/**
|
||||
* Build analytics data payload from request.
|
||||
*/
|
||||
protected function buildAnalyticsData(Request $request): array
|
||||
protected function buildAnalyticsData(Request $request): PageviewData
|
||||
{
|
||||
$data = [
|
||||
'k' => config('mattitja-analytics.site_key'),
|
||||
'u' => $request->fullUrl(),
|
||||
'ip' => $request->ip(),
|
||||
'ua' => $request->userAgent() ?? '',
|
||||
];
|
||||
|
||||
$referrer = $request->header('referer');
|
||||
if (! empty($referrer)) {
|
||||
$data['r'] = $referrer;
|
||||
}
|
||||
|
||||
return $data;
|
||||
return new PageviewData(
|
||||
siteKey: config('mattitja-analytics.site_key'),
|
||||
url: $request->fullUrl(),
|
||||
ip: $request->ip(),
|
||||
userAgent: $request->userAgent() ?? '',
|
||||
referrer: $request->header('referer'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send analytics data to the API.
|
||||
*/
|
||||
protected function sendAnalytics(array $data): void
|
||||
protected function sendAnalytics(PageviewData $data): void
|
||||
{
|
||||
try {
|
||||
app(AnalyticsClient::class)->send($data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send analytics data', [
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Mattitja\Analytics\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mattitja\Analytics\Data\PageviewData;
|
||||
|
||||
class AnalyticsClient
|
||||
{
|
||||
@@ -17,27 +18,18 @@ class AnalyticsClient
|
||||
*/
|
||||
private const int TIMEOUT = 5;
|
||||
|
||||
/**
|
||||
* Required fields for analytics data.
|
||||
*/
|
||||
private const array REQUIRED_FIELDS = ['k', 'u', 'ip', 'ua'];
|
||||
|
||||
/**
|
||||
* Send analytics data to the Mattitja Analytics API.
|
||||
*
|
||||
* @param array $data Analytics data payload
|
||||
*/
|
||||
public function send(array $data): void
|
||||
public function send(PageviewData $data): void
|
||||
{
|
||||
if (! $this->validateData($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cleanData = $this->cleanData($data);
|
||||
|
||||
try {
|
||||
Http::timeout(self::TIMEOUT)
|
||||
->post(self::ENDPOINT, $cleanData);
|
||||
->post(self::ENDPOINT, $data->toArray());
|
||||
} catch (\Exception) {
|
||||
// Fail silently to not break user experience
|
||||
}
|
||||
@@ -45,50 +37,21 @@ class AnalyticsClient
|
||||
|
||||
/**
|
||||
* Validate that all required fields are present and not empty.
|
||||
*
|
||||
* @param array $data Analytics data to validate
|
||||
* @return bool True if valid, false otherwise
|
||||
*/
|
||||
protected function validateData(array $data): bool
|
||||
protected function validateData(PageviewData $data): bool
|
||||
{
|
||||
foreach (self::REQUIRED_FIELDS as $field) {
|
||||
if (! isset($data[$field])) {
|
||||
Log::warning("Analytics: Required field '{$field}' is missing");
|
||||
if (empty($data->siteKey) || empty($data->url) || empty($data->ip)) {
|
||||
Log::warning('Analytics: Required field is missing');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// User agent can be empty string, but other fields cannot
|
||||
if ($field !== 'ua' && empty($data[$field])) {
|
||||
Log::warning("Analytics: Required field '{$field}' cannot be empty");
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// URL max length check (API spec: max 512 characters)
|
||||
if (isset($data['u']) && strlen($data['u']) > 512) {
|
||||
Log::warning('Analytics: URL exceeds 512 characters, skipping');
|
||||
if (strlen($data->url) > 512) {
|
||||
Log::warning('Analytics: URL exceeds 512 characters');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean analytics data by removing empty optional fields.
|
||||
*
|
||||
* @param array $data Raw analytics data
|
||||
* @return array Cleaned analytics data
|
||||
*/
|
||||
protected function cleanData(array $data): array
|
||||
{
|
||||
// Remove empty referrer URLs
|
||||
if (isset($data['r']) && empty($data['r'])) {
|
||||
unset($data['r']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user