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
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
- 'src/**'
- 'docs/**'
- 'examples/**'
- 'web/landing/**'
- 'README.md'
push:
branches:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,21 @@ public function test_run_db_table_list(): void

$tester->assertCommandIsSuccessful();

static::assertSame(<<<'OUTPUT'
$display = $tester->getDisplay();

static::assertStringContainsString(<<<'OUTPUT'
┌──────────┬───────────┬─────────┐
│ Name │ Namespace │ Columns │
├──────────┼───────────┼─────────┤
│ table_01 │ public │ 3 │
│ table_02 │ public │ 3 │
└──────────┴───────────┴─────────┘
------------------ -----
Summary
------------------ -----
Total tables 2
Total namespaces 1
Total columns 6
------------------ -----

OUTPUT, $display);

OUTPUT, $tester->getDisplay());
static::assertStringContainsString('Summary', $display);
static::assertStringContainsString('Total tables 2', $display);
static::assertStringContainsString('Total namespaces 1', $display);
static::assertStringContainsString('Total columns 6', $display);
}

protected function dbContext(): DatabaseContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public function test_blocking_wait_unblocks_on_concurrent_notify(): void
$listener = $this->pgsqlContext()->client;
$listener->listen('flow_test_blocking');

$this->pgsqlContext()->spawnBackgroundNotifier('flow_test_blocking', 'delivered', 200);
$this->pgsqlContext()->scheduleNotify('flow_test_blocking', 'delivered', 200);

$startNs = hrtime(true);
$notification = $listener->wait(3000);
$elapsedMs = (hrtime(true) - $startNs) / 1_000_000;

if ($notification === null) {
$stderr = implode("\n---\n", $this->pgsqlContext()->backgroundStderrContents());
static::fail('No notification received. Background sender stderr:' . "\n" . $stderr);
$errors = implode("\n---\n", $this->pgsqlContext()->backgroundNotifierErrors());
static::fail('No notification received. Scheduled notification errors:' . "\n" . $errors);
}

static::assertSame('flow_test_blocking', $notification->channel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,37 @@
namespace Flow\PostgreSql\Tests\Integration;

use Flow\PostgreSql\Client\Client;
use PgSql\Connection;
use RuntimeException;

use function fclose;
use function file_get_contents;
use function Flow\PostgreSql\DSL\and_;
use function Flow\PostgreSql\DSL\col;
use function Flow\PostgreSql\DSL\drop;
use function Flow\PostgreSql\DSL\eq;
use function Flow\PostgreSql\DSL\func;
use function Flow\PostgreSql\DSL\literal;
use function Flow\PostgreSql\DSL\notify;
use function Flow\PostgreSql\DSL\pgsql_client;
use function Flow\PostgreSql\DSL\pgsql_connection_dsn;
use function Flow\PostgreSql\DSL\select;
use function Flow\PostgreSql\DSL\table;
use function getcwd;
use function getenv;
use function is_file;
use function is_resource;
use function proc_close;
use function proc_open;
use function pg_close;
use function pg_connect;
use function pg_get_result;
use function pg_last_error;
use function pg_result_error;
use function pg_send_query;
use function sprintf;
use function sys_get_temp_dir;
use function uniqid;
use function unlink;
use function var_export;

use const PGSQL_CONNECT_FORCE_NEW;

final class PostgreSqlContext
{
public readonly Client $client;

/** @var list<resource> */
private array $backgroundProcesses = [];

/** @var list<string> */
private array $backgroundStderrLogs = [];
/** @var list<Connection> */
private array $backgroundConnections = [];

private readonly string $dsn;

Expand All @@ -59,19 +55,26 @@ public function __construct()
}

/**
* Drains the results of every scheduled notification and returns the errors
* PostgreSQL reported for them. Empty when all of them succeeded.
*
* @return list<string>
*/
public function backgroundStderrContents(): array
public function backgroundNotifierErrors(): array
{
$out = [];
$errors = [];

foreach ($this->backgroundStderrLogs as $path) {
if (is_file($path)) {
$out[] = file_get_contents($path) ?: '';
foreach ($this->backgroundConnections as $connection) {
while (($result = pg_get_result($connection)) !== false) {
$error = pg_result_error($result);

if ($error !== false && $error !== '') {
$errors[] = $error;
}
}
}

return $out;
return $errors;
}

public function client(): Client
Expand All @@ -81,17 +84,10 @@ public function client(): Client

public function close(): void
{
foreach ($this->backgroundProcesses as $process) {
proc_close($process);
}
$this->backgroundProcesses = [];

foreach ($this->backgroundStderrLogs as $path) {
if (is_file($path)) {
@unlink($path);
}
foreach ($this->backgroundConnections as $connection) {
pg_close($connection);
}
$this->backgroundStderrLogs = [];
$this->backgroundConnections = [];

foreach ($this->secondaryClients as $client) {
$client->close();
Expand Down Expand Up @@ -190,56 +186,33 @@ public function newClient(): Client
}

/**
* Spawns a detached child PHP process that sleeps for $delayMs and then
* fires a NOTIFY on $channel with $payload using its own Client
* connection. Returns the child process handle; the context closes it
* during close(). Used for integration tests that need to exercise the
* blocking wait path of waitForNotification().
* Fires a NOTIFY on $channel with $payload after $delayMs on a dedicated
* connection, without blocking the caller. The delay runs server side, so
* the notification arrives while the caller sits in a blocking wait on its
* own connection. Used by tests that exercise the blocking wait path of
* waitForNotification().
*/
public function spawnBackgroundNotifier(string $channel, string $payload, int $delayMs): void
public function scheduleNotify(string $channel, string $payload, int $delayMs): void
{
$cwd = getcwd();
$connection = pg_connect(pgsql_connection_dsn($this->dsn)->toString(), PGSQL_CONNECT_FORCE_NEW);

if ($cwd === false) {
throw new RuntimeException('Failed to determine current working directory');
if ($connection === false) {
throw new RuntimeException('Failed to open a connection for the scheduled notification');
}

$autoload = $cwd . '/vendor/autoload.php';
$this->backgroundConnections[] = $connection;

if (!is_file($autoload)) {
throw new RuntimeException(sprintf('Project vendor/autoload.php not found at %s', $autoload));
}
$sent = pg_send_query($connection, sprintf(
'%s; %s',
select(func('pg_sleep', [literal($delayMs / 1000)]))->toSql(),
notify($channel)->withPayload($payload)->toSql(),
));

$phpCode = sprintf(
'usleep(%d); require %s; try { $c = \Flow\PostgreSql\DSL\pgsql_client(\Flow\PostgreSql\DSL\pgsql_connection_dsn(%s)); $c->execute(\Flow\PostgreSql\DSL\notify(%s)->withPayload(%s)); $c->close(); } catch (\Throwable $e) { fwrite(STDERR, "child error: " . get_class($e) . ": " . $e->getMessage() . "\n"); exit(1); }',
$delayMs * 1000,
var_export($autoload, true),
var_export($this->dsn, true),
var_export($channel, true),
var_export($payload, true),
);

$stderrLog = sys_get_temp_dir() . '/flow-bg-notifier-' . uniqid('', true) . '.log';
$descriptors = [
0 => ['pipe', 'r'],
1 => ['file', '/dev/null', 'w'],
2 => ['file', $stderrLog, 'w'],
];

$pipes = [];
$process = proc_open(['php', '-r', $phpCode], $descriptors, $pipes);

if (!is_resource($process)) {
throw new RuntimeException('Failed to spawn background notifier process');
if ($sent === false) {
throw new RuntimeException(sprintf(
'Failed to send the scheduled notification: %s',
pg_last_error($connection),
));
}

if (!is_resource($pipes[0] ?? null)) {
throw new RuntimeException('Failed to open stdin pipe to background notifier process');
}

fclose($pipes[0]);

$this->backgroundProcesses[] = $process;
$this->backgroundStderrLogs[] = $stderrLog;
}
}
2 changes: 1 addition & 1 deletion web/landing/assets/styles/app.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import './changelog.css';
@import './shop-legal.css';
@import './work-shop-legal.css';

@tailwind base;
@tailwind components;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,53 @@
section.shop-legal {
section.work-shop-legal {
@apply text-slate-700 leading-relaxed dark:text-white/85;
}

section.shop-legal h1 {
section.work-shop-legal h1 {
@apply font-bold text-3xl sm:text-4xl tracking-tight text-slate-900 mt-2 mb-6 dark:text-white;
}

section.shop-legal h2 {
section.work-shop-legal h2 {
@apply font-bold text-2xl tracking-tight text-slate-900 mt-10 mb-3 dark:text-white;
}

section.shop-legal h3 {
section.work-shop-legal h3 {
@apply font-semibold text-xl text-slate-900 mt-7 mb-2 dark:text-white;
}

section.shop-legal p {
section.work-shop-legal p {
@apply mb-4;
}

section.shop-legal a {
section.work-shop-legal a {
@apply text-blue-300 underline-offset-2 hover:text-orange-300 transition-colors
dark:text-blue-100 dark:hover:text-orange-100;
}

section.shop-legal ul {
section.work-shop-legal ul {
@apply list-disc pl-6 mb-4 space-y-1.5;
}

section.shop-legal ol {
section.work-shop-legal ol {
@apply list-decimal pl-6 mb-4 space-y-1.5;
}

section.shop-legal strong {
section.work-shop-legal strong {
@apply font-semibold text-slate-900 dark:text-white;
}

section.shop-legal blockquote {
section.work-shop-legal blockquote {
@apply my-6 rounded-r-lg border-l-4 border-orange-100/70 bg-orange-100/10 px-4 py-3 text-sm muted;
}

section.shop-legal blockquote p {
section.work-shop-legal blockquote p {
@apply mb-0;
}

section.shop-legal hr {
section.work-shop-legal hr {
@apply my-8 border-0 h-px bg-slate-200 dark:bg-white/10;
}

section.shop-legal :not(pre) > code {
section.work-shop-legal :not(pre) > code {
@apply font-mono text-[0.9em] px-1.5 py-0.5 rounded bg-violet-50 text-violet-700
dark:bg-white/[0.06] dark:text-[#d2a8ff];
}
6 changes: 3 additions & 3 deletions web/landing/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ parameters:
router.request_context.scheme: '%env(SCHEME)%'
flow_version: '%env(FLOW_VERSION)%'
turnstile_appearance_default: 'interaction-only'
shop_checkout_links:
work_shop_checkout_links:
symfony_backoffice: 'https://buy.polar.sh/polar_cl_KAb6EWLVtewQhsDZV2bGeqsxSxTWKOOvl1KAq4GbDlx'

services:
Expand All @@ -27,9 +27,9 @@ services:
Flow\Website\Service\:
resource: '../src/Flow/Website/Service/'

Flow\Website\Controller\ShopController:
Flow\Website\Controller\WorkShopController:
arguments:
$checkoutLinks: '%shop_checkout_links%'
$checkoutLinks: '%work_shop_checkout_links%'

Flow\Website\Factory\Github\ContributorsRequestFactory:
arguments:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,3 @@ address, and material changes are reflected by updating the date above.
## 13. Contact

For any privacy question or request, contact us at support@flow-php.com.
Our Terms of Sale are available at [Terms of Sale](/work-shop/terms-of-sales).
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ arrangements and invoicing apply.
We process personal data needed to deliver access and provide support (such as
your email and GitHub account). Polar processes data needed for payment as
Merchant of Record. For details on how we handle personal data, see our
[Privacy Policy](/work-shop/privacy-policy). Where we list purchasers as project
Privacy Policy. Where we list purchasers as project
sponsors, we do so only for those who explicitly opt in, and only the GitHub
profile they choose to display.

Expand Down
Loading
Loading