feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\Database\Migrations;
|
||||
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Create component_state table for LiveComponents state persistence
|
||||
*
|
||||
* Stores current state of LiveComponents with metadata and tracking.
|
||||
* NOT reversible: Production table with user data - no safe rollback.
|
||||
*/
|
||||
final readonly class CreateComponentStateTable implements Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->create('component_state', function ($table) {
|
||||
// Primary Key
|
||||
$table->string('component_id', 255)->primary();
|
||||
|
||||
// State Data (encrypted)
|
||||
$table->text('state_data');
|
||||
$table->string('state_class', 255);
|
||||
|
||||
// Metadata
|
||||
$table->string('component_name', 255);
|
||||
$table->string('user_id', 255)->nullable();
|
||||
$table->string('session_id', 255)->nullable();
|
||||
|
||||
// Tracking
|
||||
$table->unsignedInteger('version')->default(1);
|
||||
$table->string('checksum', 64); // SHA256
|
||||
|
||||
// Timestamps
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index('component_name', 'idx_component_state_name');
|
||||
$table->index('user_id', 'idx_component_state_user');
|
||||
$table->index('session_id', 'idx_component_state_session');
|
||||
$table->index('expires_at', 'idx_component_state_expires');
|
||||
$table->index('updated_at', 'idx_component_state_updated');
|
||||
});
|
||||
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '2024_12_20_120000';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create component_state table for LiveComponents persistence';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\Database\Migrations;
|
||||
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Create component_state_history table for state change tracking
|
||||
*
|
||||
* Stores historical snapshots of component state changes for debugging,
|
||||
* analytics, and audit trails. Opt-in via #[TrackStateHistory] attribute.
|
||||
* NOT reversible: Historical data should be preserved.
|
||||
*/
|
||||
final readonly class CreateComponentStateHistoryTable implements Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->create('component_state_history', function ($table) {
|
||||
// Primary Key
|
||||
$table->id();
|
||||
|
||||
// Foreign Key to component_state
|
||||
$table->string('component_id', 255);
|
||||
|
||||
// State Data Snapshot
|
||||
$table->text('state_data');
|
||||
$table->string('state_class', 255);
|
||||
|
||||
// Version & Change Tracking
|
||||
$table->unsignedInteger('version');
|
||||
$table->enum('change_type', ['created', 'updated', 'deleted'])->default('updated');
|
||||
$table->json('changed_properties')->nullable(); // What changed
|
||||
|
||||
// Context
|
||||
$table->string('user_id', 255)->nullable();
|
||||
$table->string('session_id', 255)->nullable();
|
||||
$table->string('ip_address', 45)->nullable(); // IPv6 support
|
||||
$table->text('user_agent')->nullable();
|
||||
|
||||
// Checksums for integrity
|
||||
$table->string('previous_checksum', 64)->nullable();
|
||||
$table->string('current_checksum', 64);
|
||||
|
||||
// Timestamp
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
// Indexes for queries
|
||||
$table->index('component_id', 'idx_history_component');
|
||||
$table->index(['component_id', 'version'], 'idx_history_component_version');
|
||||
$table->index('created_at', 'idx_history_created');
|
||||
$table->index('change_type', 'idx_history_change_type');
|
||||
$table->index('user_id', 'idx_history_user');
|
||||
|
||||
// Foreign Key
|
||||
$table->foreign('component_id')
|
||||
->references('component_id')
|
||||
->on('component_state')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '2024_12_20_120100';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create component_state_history table for state change tracking';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\SmartLink\Repositories;
|
||||
|
||||
use App\Domain\SmartLink\Entities\ClickEvent;
|
||||
use App\Domain\SmartLink\Repositories\ClickEventRepository;
|
||||
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
|
||||
use App\Framework\Attributes\DefaultImplementation;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\ValueObjects\QueryParameters;
|
||||
use App\Framework\Database\ValueObjects\SqlQuery;
|
||||
|
||||
#[DefaultImplementation]
|
||||
final readonly class DatabaseClickEventRepository implements ClickEventRepository
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(ClickEvent $event): void
|
||||
{
|
||||
// Implementation coming from existing code
|
||||
$this->connection->insert('smartlink_click_events', [
|
||||
'id' => $event->id->toString(),
|
||||
'smartlink_id' => $event->smartLinkId->toString(),
|
||||
'ip_hash' => $event->ipHash,
|
||||
'user_agent' => $event->userAgent,
|
||||
'referer' => $event->referer,
|
||||
'country_code' => $event->countryCode,
|
||||
'device_type' => $event->deviceType,
|
||||
'is_conversion' => $event->isConversion ? 1 : 0,
|
||||
'clicked_at' => $event->clickedAt->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function findByLinkId(SmartLinkId $linkId, ?Timestamp $since = null): array
|
||||
{
|
||||
$query = 'SELECT * FROM smartlink_click_events WHERE smartlink_id = ?';
|
||||
$params = [$linkId->toString()];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= ' AND clicked_at >= ?';
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY clicked_at DESC';
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function getRecentClicksByIpHash(string $ipHash, int $minutes = 60): array
|
||||
{
|
||||
$since = Timestamp::now()->subtractMinutes($minutes);
|
||||
|
||||
$sqlQuery = new SqlQuery(
|
||||
'SELECT * FROM smartlink_click_events WHERE ip_hash = ? AND clicked_at >= ? ORDER BY clicked_at DESC',
|
||||
new QueryParameters([$ipHash, $since->format('Y-m-d H:i:s')])
|
||||
);
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function countByLinkId(SmartLinkId $linkId): int
|
||||
{
|
||||
$sqlQuery = new SqlQuery(
|
||||
'SELECT COUNT(*) as count FROM smartlink_click_events WHERE smartlink_id = ?',
|
||||
new QueryParameters([$linkId->toString()])
|
||||
);
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
|
||||
public function countUniqueByLinkId(SmartLinkId $linkId): int
|
||||
{
|
||||
$sqlQuery = new SqlQuery(
|
||||
'SELECT COUNT(DISTINCT ip_hash) as count FROM smartlink_click_events WHERE smartlink_id = ?',
|
||||
new QueryParameters([$linkId->toString()])
|
||||
);
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
|
||||
public function getConversionsByLinkId(SmartLinkId $linkId): int
|
||||
{
|
||||
$sqlQuery = new SqlQuery(
|
||||
'SELECT COUNT(*) as count FROM smartlink_click_events WHERE smartlink_id = ? AND is_conversion = TRUE',
|
||||
new QueryParameters([$linkId->toString()])
|
||||
);
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
|
||||
// Analytics Aggregation Methods
|
||||
|
||||
public function getClickTimeSeriesByHour(Timestamp $since): array
|
||||
{
|
||||
$sqlQuery = new SqlQuery(
|
||||
"SELECT
|
||||
DATE_FORMAT(clicked_at, '%Y-%m-%d %H:00:00') as hour,
|
||||
COUNT(*) as clicks,
|
||||
COUNT(DISTINCT ip_hash) as unique_clicks
|
||||
FROM smartlink_click_events
|
||||
WHERE clicked_at >= ?
|
||||
GROUP BY DATE_FORMAT(clicked_at, '%Y-%m-%d %H:00:00')
|
||||
ORDER BY hour ASC",
|
||||
new QueryParameters([$since->format('Y-m-d H:i:s')])
|
||||
);
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function getGeographicDistribution(?Timestamp $since = null): array
|
||||
{
|
||||
$query = "SELECT
|
||||
country_code,
|
||||
COUNT(*) as click_count
|
||||
FROM smartlink_click_events
|
||||
WHERE country_code IS NOT NULL";
|
||||
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " AND clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$query .= " GROUP BY country_code ORDER BY click_count DESC";
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function getDeviceTypeDistribution(?Timestamp $since = null): array
|
||||
{
|
||||
$query = "SELECT
|
||||
device_type,
|
||||
COUNT(*) as click_count
|
||||
FROM smartlink_click_events
|
||||
WHERE device_type IS NOT NULL";
|
||||
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " AND clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$query .= " GROUP BY device_type ORDER BY click_count DESC";
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function getTopLinksByClicks(int $limit, ?Timestamp $since = null): array
|
||||
{
|
||||
$query = "SELECT
|
||||
smartlink_id as link_id,
|
||||
COUNT(*) as click_count,
|
||||
COUNT(DISTINCT ip_hash) as unique_click_count
|
||||
FROM smartlink_click_events";
|
||||
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " WHERE clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$query .= " GROUP BY smartlink_id ORDER BY click_count DESC LIMIT ?";
|
||||
$params[] = $limit;
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
return $this->connection->query($sqlQuery)->fetchAll();
|
||||
}
|
||||
|
||||
public function countTotal(?Timestamp $since = null): int
|
||||
{
|
||||
$query = "SELECT COUNT(*) as count FROM smartlink_click_events";
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " WHERE clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
|
||||
public function countUniqueTotal(?Timestamp $since = null): int
|
||||
{
|
||||
$query = "SELECT COUNT(DISTINCT ip_hash) as count FROM smartlink_click_events";
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " WHERE clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
|
||||
public function countConversionsTotal(?Timestamp $since = null): int
|
||||
{
|
||||
$query = "SELECT COUNT(*) as count FROM smartlink_click_events WHERE is_conversion = TRUE";
|
||||
$params = [];
|
||||
|
||||
if ($since !== null) {
|
||||
$query .= " AND clicked_at >= ?";
|
||||
$params[] = $since->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$sqlQuery = new SqlQuery($query, new QueryParameters($params));
|
||||
$result = $this->connection->query($sqlQuery)->fetchAll();
|
||||
|
||||
return (int) ($result[0]['count'] ?? 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user