Author: Shivam Mathur (shivammathur)
Committer: GitHub (web-flow)
Pusher: shivammathur
Date: 2025-09-24T19:43:54+05:30

Commit: 
https://github.com/php/web-downloads/commit/840e683a6dd3b85f71604134b2f4346368ff4e20
Raw diff: 
https://github.com/php/web-downloads/commit/840e683a6dd3b85f71604134b2f4346368ff4e20.diff

Add API to init series files (#4)

Changed paths:
  A  src/Console/Command/SeriesInitCommand.php
  A  src/Http/Controllers/SeriesInitController.php
  A  tests/Console/Command/SeriesInitCommandTest.php
  A  tests/Http/Controllers/SeriesInitControllerTest.php
  M  routes.php


Diff:

diff --git a/routes.php b/routes.php
index 7491aa7..d8a5471 100644
--- a/routes.php
+++ b/routes.php
@@ -3,6 +3,7 @@
 use App\Http\Controllers\IndexController;
 use App\Http\Controllers\PeclController;
 use App\Http\Controllers\PhpController;
+use App\Http\Controllers\SeriesInitController;
 use App\Http\Controllers\WinlibsController;
 use App\Router;
 
@@ -11,4 +12,5 @@
 $router->registerRoute('/api/pecl', 'POST', PeclController::class, true);
 $router->registerRoute('/api/winlibs', 'POST', WinlibsController::class, true);
 $router->registerRoute('/api/php', 'POST', PhpController::class, true);
+$router->registerRoute('/api/series-init', 'POST', 
SeriesInitController::class, true);
 $router->handleRequest();
diff --git a/src/Console/Command/SeriesInitCommand.php 
b/src/Console/Command/SeriesInitCommand.php
new file mode 100644
index 0000000..6d6405d
--- /dev/null
+++ b/src/Console/Command/SeriesInitCommand.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace App\Console\Command;
+
+use App\Console\Command;
+use Exception;
+
+class SeriesInitCommand extends Command
+{
+    protected string $signature = 'series:init --base-directory= 
--builds-directory=';
+    protected string $description = 'Initialize series files for libraries';
+
+    protected ?string $baseDirectory = null;
+
+    public function handle(): int
+    {
+        try {
+            $this->baseDirectory = $this->getOption('base-directory');
+            if (!$this->baseDirectory) {
+                throw new Exception('Base directory is required');
+            }
+
+            $buildsDirectory = $this->getOption('builds-directory');
+            if (!$buildsDirectory) {
+                throw new Exception('Build directory is required');
+            }
+
+            $series_directory = $buildsDirectory . '/series';
+            if(!is_dir($series_directory)) {
+                return Command::SUCCESS;
+            }
+
+            $files = glob($series_directory . '/series-init-*.json');
+
+            // We lock the files we are working on
+            // so that we don't process them again if the command is run again
+            $filteredFiles = [];
+            foreach ($files as $filepath) {
+                $lockFile = $filepath . '.lock';
+                if (!file_exists($lockFile)) {
+                    touch($lockFile);
+                    $filteredFiles[] = $filepath;
+                }
+            }
+
+            foreach ($filteredFiles as $filepath) {
+                $data = json_decode(file_get_contents($filepath), true, 512, 
JSON_THROW_ON_ERROR);
+                extract($data);
+                $this->initSeriesFiles($series, $series_vs, $target_vs);
+                unlink($filepath);
+                unlink($filepath . '.lock');
+            }
+            return Command::SUCCESS;
+        } catch (Exception $e) {
+            echo $e->getMessage();
+            return Command::FAILURE;
+        }
+    }
+
+    /**
+     * @throws Exception
+     */
+    private function initSeriesFiles(
+        string $series,
+        string $series_vs,
+        string $target_vs
+    ): void
+    {
+        $baseDirectory = $this->baseDirectory . "/php-sdk/deps/series";
+
+        if (!is_dir($baseDirectory)) {
+            mkdir($baseDirectory, 0755, true);
+        }
+        foreach(['x86', 'x64'] as $arch) {
+            foreach(['stable', 'staging'] as $stability) {
+                $sourceSeries = 'packages-master-' . $series_vs . '-' . $arch 
. '-' . $stability . '.txt';
+                if(!file_exists($baseDirectory . '/' . $sourceSeries)) {
+                    throw new Exception("$baseDirectory/$sourceSeries not 
found");
+                }
+                $destinationFileName = 'packages-' . $series . '-' . 
$target_vs . '-' . $arch . '-' . $stability . '.txt';
+                copy($baseDirectory . '/' . $sourceSeries, $baseDirectory . 
'/' . $destinationFileName);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Http/Controllers/SeriesInitController.php 
b/src/Http/Controllers/SeriesInitController.php
new file mode 100644
index 0000000..eeab09b
--- /dev/null
+++ b/src/Http/Controllers/SeriesInitController.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\BaseController;
+use App\Validator;
+
+class SeriesInitController extends BaseController
+{
+    protected function validate(array $data): bool
+    {
+        $validator = new Validator([
+            'series' => 'required|string',
+            'series_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
+            'target_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
+            'token' => 'required|string',
+        ]);
+
+        $validator->validate($data);
+
+        $valid = $validator->isValid();
+
+        if (!$valid) {
+            http_response_code(400);
+            echo 'Invalid request: ' . $validator;
+        }
+
+        return $valid;
+    }
+
+    protected function execute(array $data): void
+    {
+        extract($data);
+        $directory = getenv('BUILDS_DIRECTORY') . '/series';
+        $hash = hash('sha256', $series) . strtotime('now');
+        $file = $directory . '/series-init-' . $hash . '.json';
+        file_put_contents($file, json_encode($data));
+    }
+}
\ No newline at end of file
diff --git a/tests/Console/Command/SeriesInitCommandTest.php 
b/tests/Console/Command/SeriesInitCommandTest.php
new file mode 100644
index 0000000..8b39e8f
--- /dev/null
+++ b/tests/Console/Command/SeriesInitCommandTest.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Console\Command;
+
+use App\Console\Command\SeriesInitCommand;
+use App\Helpers\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+
+class SeriesInitCommandTest extends TestCase
+{
+    private string $baseDirectory;
+    private string $buildsDirectory;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->baseDirectory   = sys_get_temp_dir() . '/series_init_base_' . 
uniqid();
+        $this->buildsDirectory = sys_get_temp_dir() . '/series_init_builds_' . 
uniqid();
+
+        mkdir($this->baseDirectory, 0755, true);
+        mkdir($this->buildsDirectory, 0755, true);
+    }
+
+    protected function tearDown(): void
+    {
+        (new Helpers)->rmdirr($this->baseDirectory);
+        (new Helpers)->rmdirr($this->buildsDirectory);
+        parent::tearDown();
+    }
+
+    public static function caseProvider(): array
+    {
+        return [
+            ['8.3', 'vs17', 'vs17'],
+            ['8.2', 'vs16', 'vs16'],
+        ];
+    }
+
+    public function testReturnsSuccessWhenNoSeriesDir(): void
+    {
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        $result = $command->handle();
+
+        $this->assertSame(0, $result, 'Should return success when there is no 
builds/series directory.');
+    }
+
+    public function testMissingBaseDirectory(): void
+    {
+        $command = new SeriesInitCommand();
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        ob_start();
+        $result = $command->handle();
+        $output = trim(ob_get_clean());
+
+        $this->assertSame(1, $result);
+        $this->assertSame('Base directory is required', $output);
+    }
+
+    public function testMissingBuildsDirectory(): void
+    {
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        ob_start();
+        $result = $command->handle();
+        $output = trim(ob_get_clean());
+
+        $this->assertSame(1, $result);
+        $this->assertSame('Build directory is required', $output);
+    }
+
+    #[DataProvider('caseProvider')]
+    public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string 
$series, string $series_vs, string $target_vs): void
+    {
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        mkdir($seriesBase, 0755, true);
+
+        $sourceNames = [
+            "packages-master-$series_vs-x86-stable.txt",
+            "packages-master-$series_vs-x86-staging.txt",
+            "packages-master-$series_vs-x64-stable.txt",
+            "packages-master-$series_vs-x64-staging.txt",
+        ];
+        foreach ($sourceNames as $name) {
+            file_put_contents($seriesBase . '/' . $name, "content-of-$name");
+        }
+
+        $buildSeriesDir = $this->buildsDirectory . '/series';
+        mkdir($buildSeriesDir, 0755, true);
+        $jsonPath = $buildSeriesDir . '/series-init-task1.json';
+        file_put_contents($jsonPath, json_encode([
+            'series'    => $series,
+            'series_vs' => $series_vs,
+            'target_vs' => $target_vs,
+        ]));
+
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        $result = $command->handle();
+        $this->assertSame(0, $result, 'Command should return success.');
+
+        $destinations = [
+            "packages-$series-$target_vs-x86-stable.txt",
+            "packages-$series-$target_vs-x86-staging.txt",
+            "packages-$series-$target_vs-x64-stable.txt",
+            "packages-$series-$target_vs-x64-staging.txt",
+        ];
+
+        foreach ($destinations as $dest) {
+            $destPath = $seriesBase . '/' . $dest;
+            $this->assertFileExists($destPath);
+            $srcName = str_replace("packages-$series-$target_vs", 
"packages-master-$series_vs", $dest);
+            $this->assertSame(
+                "content-of-$srcName",
+                file_get_contents($destPath),
+                "Destination $dest should have the same content as $srcName"
+            );
+        }
+
+        $this->assertFileDoesNotExist($jsonPath);
+        $this->assertFileDoesNotExist($jsonPath . '.lock');
+    }
+
+    public function testSkipsLockedJsonFile(): void
+    {
+        $series = '8.3';
+        $series_vs = 'vs17';
+        $target_vs = 'vs17';
+
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        mkdir($seriesBase, 0755, true);
+        foreach ([
+                     "packages-master-$series_vs-x86-stable.txt",
+                     "packages-master-$series_vs-x86-staging.txt",
+                     "packages-master-$series_vs-x64-stable.txt",
+                     "packages-master-$series_vs-x64-staging.txt",
+                 ] as $name) {
+            file_put_contents($seriesBase . '/' . $name, "ok");
+        }
+
+        $buildSeriesDir = $this->buildsDirectory . '/series';
+        mkdir($buildSeriesDir, 0755, true);
+        $jsonPath = $buildSeriesDir . '/series-init-locked.json';
+        file_put_contents($jsonPath, json_encode([
+            'series'    => $series,
+            'series_vs' => $series_vs,
+            'target_vs' => $target_vs,
+        ]));
+        touch($jsonPath . '.lock');
+
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        $result = $command->handle();
+        $this->assertSame(0, $result);
+
+        $this->assertFileExists($jsonPath);
+        $this->assertFileExists($jsonPath . '.lock');
+
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x86-stable.txt");
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x86-staging.txt");
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x64-stable.txt");
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x64-staging.txt");
+    }
+
+    public function testHandlesCorruptJson(): void
+    {
+        $seriesDir = $this->buildsDirectory . '/series';
+        mkdir($seriesDir, 0755, true);
+        $jsonPath = $seriesDir . '/series-init-bad.json';
+        file_put_contents($jsonPath, '{corrupt json');
+
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+
+        $this->assertSame(1, $result);
+        $this->assertStringContainsString('Syntax error', $output);
+        $this->assertFileExists($jsonPath);
+        $this->assertFileExists($jsonPath . '.lock', 'Lock is created before 
processing and should remain after failure.');
+    }
+
+    public function testFailsWhenAnySourceSeriesFileIsMissing(): void
+    {
+        $series = '8.3';
+        $series_vs = 'vs17';
+        $target_vs = 'vs17';
+
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        mkdir($seriesBase, 0755, true);
+
+        file_put_contents($seriesBase . 
"/packages-master-$series_vs-x86-stable.txt", 'ok');
+
+        $seriesDir = $this->buildsDirectory . '/series';
+        mkdir($seriesDir, 0755, true);
+        $jsonPath = $seriesDir . '/series-init-task.json';
+        file_put_contents($jsonPath, json_encode([
+            'series'    => $series,
+            'series_vs' => $series_vs,
+            'target_vs' => $target_vs,
+        ]));
+
+        $command = new SeriesInitCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->setOption('builds-directory', $this->buildsDirectory);
+
+        ob_start();
+        $result = $command->handle();
+        $output = trim(ob_get_clean());
+
+        $this->assertSame(1, $result);
+        $this->assertStringContainsString(
+            "$seriesBase/packages-master-$series_vs-x86-staging.txt not found",
+            $output
+        );
+
+        $this->assertFileExists($seriesBase . 
"/packages-$series-$target_vs-x86-stable.txt");
+
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x86-staging.txt");
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x64-stable.txt");
+        $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x64-staging.txt");
+
+        $this->assertFileExists($jsonPath);
+        $this->assertFileExists($jsonPath . '.lock');
+    }
+}
diff --git a/tests/Http/Controllers/SeriesInitControllerTest.php 
b/tests/Http/Controllers/SeriesInitControllerTest.php
new file mode 100644
index 0000000..1aa7db3
--- /dev/null
+++ b/tests/Http/Controllers/SeriesInitControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\SeriesInitController;
+use PHPUnit\Framework\TestCase;
+
+class MockSeriesInitController extends SeriesInitController {
+    protected function validate(array $data): bool {
+        return isset($data['key']);
+    }
+
+    protected function execute(array $data): void {
+        echo "Executed";
+    }
+
+    public function handle(): void
+    {
+        $data = json_decode(file_get_contents($this->inputPath), true);
+
+        if ($this->validate($data)) {
+            $this->execute($data);
+        }
+    }
+}
+
+class SeriesInitControllerTest extends TestCase {
+    public function testHandleWithValidData() {
+        $data = json_encode(["key" => "value"]);
+        $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+        file_put_contents($tempFile, $data);
+        $controller = new MockSeriesInitController($tempFile);
+        $this->expectOutputString("Executed");
+        $controller->handle();
+        unlink($tempFile);
+    }
+}

Reply via email to