Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ desktop.ini
/storage/*.sqlite
/storage/*.sqlite3
/storage/*.db
/examples/**/storage/*.sqlite
/examples/**/storage/*.sqlite3
/examples/**/storage/*.db

node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

/.phpunit.cache
/.phpunit.cache
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,33 @@ http://127.0.0.1:8002

PrivateChat demonstrates global chat, direct 1:1 conversations, private group rooms, unread badges, typing indicators and simple message receipts.

## Optional storage adapters

PHPSockets currently uses in-memory storage by default for the examples.

The package also includes optional storage adapters:

```txt
InMemory
File JSONL messages
PDO SQLite
PDO MySQL
PDO PostgreSQL
```

SQLite can be initialized programmatically with the migration runner:

```php
use Micilini\PhpSockets\Database\MigrationRunner;
use Micilini\PhpSockets\Storage\Pdo\PdoConnectionFactory;

$pdo = PdoConnectionFactory::sqlite(__DIR__ . '/storage/phpsockets.sqlite');

(new MigrationRunner($pdo))->run('sqlite');
```

The CLI migration command will be added in a future phase.

## Requirements

The modern version targets:
Expand All @@ -136,6 +163,7 @@ The modern version targets:
Optional future features may require:

- `ext-pdo` for SQL storage adapters.
- `ext-pdo_sqlite` for SQLite storage tests and local persistence.
- Laravel packages for optional Laravel integration.

## Namespace
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"suggest": {
"ext-pdo": "Required for SQL storage adapters and migrations.",
"ext-pdo_sqlite": "Required for SQLite storage tests and local persistence.",
"illuminate/support": "Required for Laravel integration."
},
"autoload": {
Expand All @@ -48,4 +49,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}
6 changes: 6 additions & 0 deletions examples/private-chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ PrivateChat displays unread badges for Global Room, direct conversations and pri

Badges increase while a conversation is not open and reset when the conversation is opened.

## Storage note

This example still uses in-memory storage by default.

The package now includes optional storage adapters and migrations, but the official CLI/config workflow is added in a later phase.

## Important notes

This phase implements direct 1:1 private messaging and private group rooms.
45 changes: 45 additions & 0 deletions src/Database/MigrationRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Micilini\PhpSockets\Database;

use Micilini\PhpSockets\Exceptions\StorageException;
use PDO;

final readonly class MigrationRunner
{
public function __construct(
private PDO $pdo,
private ?string $schemaPath = null,
) {
}

public function run(string $driver): void
{
$this->pdo->exec($this->schemaSql($driver));
}

private function schemaSql(string $driver): string
{
$driver = strtolower(trim($driver));

if (!in_array($driver, ['sqlite', 'mysql', 'pgsql'], true)) {
throw new StorageException("Unsupported migration driver: {$driver}");
}

$path = $this->schemaPath ?? dirname(__DIR__) . '/Database/Schema/' . $driver . '.sql';

if (!is_file($path)) {
throw new StorageException("Migration schema file not found: {$path}");
}

$sql = file_get_contents($path);

if (!is_string($sql) || trim($sql) === '') {
throw new StorageException("Migration schema file is empty: {$path}");
}

return $sql;
}
}
59 changes: 59 additions & 0 deletions src/Database/Schema/mysql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(120) NOT NULL,
normalized_display_name VARCHAR(120) NOT NULL,
created_at VARCHAR(40) NOT NULL,
UNIQUE KEY idx_users_normalized_display_name (normalized_display_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
connected TINYINT(1) NOT NULL DEFAULT 0,
connected_at VARCHAR(40) NOT NULL,
last_seen_at VARCHAR(40) NOT NULL,
INDEX idx_sessions_user_id (user_id),
INDEX idx_sessions_connected (connected),
CONSTRAINT fk_sessions_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS rooms (
id VARCHAR(64) PRIMARY KEY,
type VARCHAR(40) NOT NULL,
name VARCHAR(120) NULL,
created_by VARCHAR(64) NOT NULL,
created_at VARCHAR(40) NOT NULL,
INDEX idx_rooms_type (type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS room_members (
room_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
joined_at VARCHAR(40) NOT NULL,
PRIMARY KEY (room_id, user_id),
INDEX idx_room_members_user_id (user_id),
CONSTRAINT fk_room_members_room_id FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS messages (
id VARCHAR(64) PRIMARY KEY,
room_id VARCHAR(64) NOT NULL,
from_user_id VARCHAR(64) NOT NULL,
kind VARCHAR(40) NOT NULL,
body TEXT NULL,
metadata_json JSON NOT NULL,
created_at VARCHAR(40) NOT NULL,
INDEX idx_messages_room_created (room_id, created_at),
CONSTRAINT fk_messages_room_id FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS attachments (
id VARCHAR(64) PRIMARY KEY,
message_id VARCHAR(64) NOT NULL,
filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(120) NOT NULL,
size_bytes BIGINT NOT NULL,
path TEXT NOT NULL,
created_at VARCHAR(40) NOT NULL,
CONSTRAINT fk_attachments_message_id FOREIGN KEY (message_id) REFERENCES messages (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
64 changes: 64 additions & 0 deletions src/Database/Schema/pgsql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(120) NOT NULL,
normalized_display_name VARCHAR(120) NOT NULL UNIQUE,
created_at VARCHAR(40) NOT NULL
);

CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL REFERENCES users (id) ON DELETE CASCADE,
connected BOOLEAN NOT NULL DEFAULT FALSE,
connected_at VARCHAR(40) NOT NULL,
last_seen_at VARCHAR(40) NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_sessions_user_id
ON sessions (user_id);

CREATE INDEX IF NOT EXISTS idx_sessions_connected
ON sessions (connected);

CREATE TABLE IF NOT EXISTS rooms (
id VARCHAR(64) PRIMARY KEY,
type VARCHAR(40) NOT NULL,
name VARCHAR(120) NULL,
created_by VARCHAR(64) NOT NULL,
created_at VARCHAR(40) NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_rooms_type
ON rooms (type);

CREATE TABLE IF NOT EXISTS room_members (
room_id VARCHAR(64) NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
user_id VARCHAR(64) NOT NULL,
joined_at VARCHAR(40) NOT NULL,
PRIMARY KEY (room_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_room_members_user_id
ON room_members (user_id);

CREATE TABLE IF NOT EXISTS messages (
id VARCHAR(64) PRIMARY KEY,
room_id VARCHAR(64) NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
from_user_id VARCHAR(64) NOT NULL,
kind VARCHAR(40) NOT NULL,
body TEXT NULL,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at VARCHAR(40) NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_messages_room_created
ON messages (room_id, created_at);

CREATE TABLE IF NOT EXISTS attachments (
id VARCHAR(64) PRIMARY KEY,
message_id VARCHAR(64) NOT NULL REFERENCES messages (id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(120) NOT NULL,
size_bytes BIGINT NOT NULL,
path TEXT NOT NULL,
created_at VARCHAR(40) NOT NULL
);
71 changes: 71 additions & 0 deletions src/Database/Schema/sqlite.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
normalized_display_name TEXT NOT NULL,
created_at TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_users_normalized_display_name
ON users (normalized_display_name);

CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
connected INTEGER NOT NULL DEFAULT 0,
connected_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_sessions_user_id
ON sessions (user_id);

CREATE INDEX IF NOT EXISTS idx_sessions_connected
ON sessions (connected);

CREATE TABLE IF NOT EXISTS rooms (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NULL,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_rooms_type
ON rooms (type);

CREATE TABLE IF NOT EXISTS room_members (
room_id TEXT NOT NULL,
user_id TEXT NOT NULL,
joined_at TEXT NOT NULL,
PRIMARY KEY (room_id, user_id),
FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_room_members_user_id
ON room_members (user_id);

CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
room_id TEXT NOT NULL,
from_user_id TEXT NOT NULL,
kind TEXT NOT NULL,
body TEXT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_messages_room_created
ON messages (room_id, created_at);

CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
path TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (message_id) REFERENCES messages (id) ON DELETE CASCADE
);
11 changes: 11 additions & 0 deletions src/Exceptions/StorageException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Micilini\PhpSockets\Exceptions;

use RuntimeException;

final class StorageException extends RuntimeException
{
}
Loading
Loading