feat(figma): add version author id and robust incremental sync pagination

Improve Figma synchronization to persist version author identity and handle API pagination links reliably during incremental sync jobs.

Changes included:
- Added  support end-to-end:
  - New migration  adds column and index.
  -  now allows .
  -  maps user id from Figma version payload ( / ).
  -  snapshot query now selects .
  -  displays Figma User ID column and updates table colspan states.
- Hardened Figma pagination flow in :
  - Follow absolute  URLs when returned by API.
  - Track seen pagination URLs to prevent loops.
  - Support additional page-size query key and URL-safe encoding.
  - Updated request builder to work with absolute endpoints and existing query strings.
- Consolidated schema intent:
  - Moved Volume in drive C: has no label
Volume Serial Number is 2B45-1F84/ columns into initial Figma table migration.
  - Removed superseded follow-up migrations that previously added/dropped these fields.
- Updated project docs and dependencies:
  - Replaced starter README with CRM Summit specific project README.
  - Refreshed  (framework and dependency version bumps).
- Added database artifact: .

Impact:
- Incremental sync should now process paginated version/history feeds more reliably.
- Snapshot API and dashboard expose author-level metadata for auditing and filtering.
- Fresh installs get cleaner Figma schema history from baseline migration.
This commit is contained in:
mahdahar 2026-04-28 05:39:02 +07:00
parent 329e4e6725
commit 6776d539ae
11 changed files with 262 additions and 256 deletions

View File

@ -1,68 +1,64 @@
# CodeIgniter 4 Application Starter # CRM Summit
## What is CodeIgniter? CRM Summit is a CodeIgniter 4 app for CRM, service, product, certificate, and support workflows.
CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. ## Main modules
More information can be found at the [official site](https://codeigniter.com).
This repository holds a composer-installable app starter. - Auth and user management
It has been built from the - Dashboard
[development repository](https://github.com/codeigniter4/CodeIgniter4). - Accounts, sites, offices, zones, and areas
- Products, product types, services, aliases, catalogs, and temp import flow
- Activities, emails, contacts, and mail groups
- Bugs and bug comments
- Certificates: maintenance, installation, training
- Guidebook and inventory counters/transactions
- Integrations and sync jobs for Figma and Gitea
More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. ## Tech stack
You can read the [user guide](https://codeigniter.com/user_guide/) - PHP 8.1+
corresponding to the latest version of the framework. - CodeIgniter 4
- MySQL/MariaDB
## Installation & updates - dompdf for PDF output
- endroid/qr-code for QR generation
`composer create-project codeigniter4/appstarter` then `composer update` whenever - ramsey/uuid for UUID support
there is a new release of the framework.
When updating, check the release notes to see if there are any changes you might need to apply
to your `app` folder. The affected files can be copied or merged from
`vendor/codeigniter4/framework/app`.
## Setup ## Setup
Copy `env` to `.env` and tailor for your app, specifically the baseURL 1. Install dependencies:
and any database settings.
## Important Change with index.php ```bash
composer install
```
`index.php` is no longer in the root of the project! It has been moved inside the *public* folder, 2. Copy `env` to `.env` and set app and database values:
for better security and separation of components.
This means that you should configure your web server to "point" to your project's *public* folder, and ```bash
not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the copy env .env
framework are exposed. ```
**Please** read the user guide for a better explanation of how CI4 works! 3. Set `app.baseURL` for local or production host.
## Repository Management 4. Point web server to `public/` folder.
We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. 5. Run migrations if project uses them:
We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss
FEATURE REQUESTS.
This repository is a "distribution" one, built by our release preparation script. ```bash
Problems with it can be raised on our forum, or as issues in the main repository. php spark migrate
```
## Server Requirements ## Development commands
PHP version 8.1 or higher is required, with the following extensions installed: ```bash
php spark list
php spark cache:clear
php spark migrate
php spark migrate:rollback
vendor\bin\phpunit
```
- [intl](http://php.net/manual/en/intl.requirements.php) ## Notes
- [mbstring](http://php.net/manual/en/mbstring.installation.php)
> [!WARNING] - `index.php` lives in `public/` for security.
> - The end of life date for PHP 7.4 was November 28, 2022. - Check `app/Config/Routes.php` for module URLs and entry points.
> - The end of life date for PHP 8.0 was November 26, 2023. - Use `app/Config/` and `.env` for environment-specific settings.
> - If you are still using PHP 7.4 or 8.0, you should upgrade immediately.
> - The end of life date for PHP 8.1 will be December 31, 2025.
Additionally, make sure that the following extensions are enabled in your PHP:
- json (enabled by default - don't turn it off)
- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL
- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library

View File

@ -103,7 +103,7 @@ class FigmaApi extends BaseController
$total = (int) (clone $baseBuilder)->countAllResults(); $total = (int) (clone $baseBuilder)->countAllResults();
$rows = $baseBuilder $rows = $baseBuilder
->select('v.id, v.figma_version_id, v.version, v.label, v.description, v.name, v.editor_type, v.last_modified_figma, v.created_at_figma, f.file_key, f.last_synced_at') ->select('v.id, v.figma_version_id, v.version, v.label, v.description, v.name, v.editor_type, v.figma_user_id, v.last_modified_figma, v.created_at_figma, f.file_key, f.last_synced_at')
->orderBy('v.created_at_figma', 'DESC') ->orderBy('v.created_at_figma', 'DESC')
->limit($perPage, $offset) ->limit($perPage, $offset)
->get() ->get()
@ -177,7 +177,7 @@ class FigmaApi extends BaseController
} }
$service = new FigmaSyncService(); $service = new FigmaSyncService();
$result = $service->syncAll(); $result = $service->syncIncremental(1);
$statusCode = $result['success'] ? 200 : 500; $statusCode = $result['success'] ? 200 : 500;
return $this->respond([ return $this->respond([

View File

@ -28,6 +28,15 @@ class CreateFigmaTables extends Migration
'constraint' => 255, 'constraint' => 255,
'null' => true, 'null' => true,
], ],
'label' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'description' => [
'type' => 'LONGTEXT',
'null' => true,
],
'last_modified' => [ 'last_modified' => [
'type' => 'DATETIME', 'type' => 'DATETIME',
'null' => true, 'null' => true,
@ -75,6 +84,15 @@ class CreateFigmaTables extends Migration
'constraint' => 255, 'constraint' => 255,
'null' => true, 'null' => true,
], ],
'label' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'description' => [
'type' => 'LONGTEXT',
'null' => true,
],
'name' => [ 'name' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 255, 'constraint' => 255,

View File

@ -1,43 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddLabelDescriptionToFigmaVersions extends Migration
{
public function up()
{
$fields = [];
if (!$this->db->fieldExists('label', 'figma_file_versions')) {
$fields['label'] = [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
];
}
if (!$this->db->fieldExists('description', 'figma_file_versions')) {
$fields['description'] = [
'type' => 'LONGTEXT',
'null' => true,
];
}
if (!empty($fields)) {
$this->forge->addColumn('figma_file_versions', $fields);
}
}
public function down()
{
if ($this->db->fieldExists('description', 'figma_file_versions')) {
$this->forge->dropColumn('figma_file_versions', 'description');
}
if ($this->db->fieldExists('label', 'figma_file_versions')) {
$this->forge->dropColumn('figma_file_versions', 'label');
}
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RemoveFileUrlAndThumbnailFromFigmaTables extends Migration
{
public function up()
{
if ($this->db->fieldExists('file_url', 'figma_files')) {
$this->forge->dropColumn('figma_files', 'file_url');
}
if ($this->db->fieldExists('thumbnail_url', 'figma_files')) {
$this->forge->dropColumn('figma_files', 'thumbnail_url');
}
if ($this->db->fieldExists('file_url', 'figma_file_versions')) {
$this->forge->dropColumn('figma_file_versions', 'file_url');
}
if ($this->db->fieldExists('thumbnail_url', 'figma_file_versions')) {
$this->forge->dropColumn('figma_file_versions', 'thumbnail_url');
}
}
public function down()
{
$fieldsFiles = [];
if (!$this->db->fieldExists('file_url', 'figma_files')) {
$fieldsFiles['file_url'] = [
'type' => 'TEXT',
'null' => true,
];
}
if (!$this->db->fieldExists('thumbnail_url', 'figma_files')) {
$fieldsFiles['thumbnail_url'] = [
'type' => 'TEXT',
'null' => true,
];
}
if (!empty($fieldsFiles)) {
$this->forge->addColumn('figma_files', $fieldsFiles);
}
$fieldsVersions = [];
if (!$this->db->fieldExists('file_url', 'figma_file_versions')) {
$fieldsVersions['file_url'] = [
'type' => 'TEXT',
'null' => true,
];
}
if (!$this->db->fieldExists('thumbnail_url', 'figma_file_versions')) {
$fieldsVersions['thumbnail_url'] = [
'type' => 'TEXT',
'null' => true,
];
}
if (!empty($fieldsVersions)) {
$this->forge->addColumn('figma_file_versions', $fieldsVersions);
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddFigmaUserIdToFileVersions extends Migration
{
public function up()
{
$this->forge->addColumn('figma_file_versions', [
'figma_user_id' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
]);
$this->db->query('CREATE INDEX idx_figma_file_versions_figma_user_id ON figma_file_versions(figma_user_id)');
}
public function down()
{
$this->db->query('DROP INDEX idx_figma_file_versions_figma_user_id ON figma_file_versions');
$this->forge->dropColumn('figma_file_versions', 'figma_user_id');
}
}

View File

@ -183,6 +183,7 @@ class FigmaSyncService
$label = $version['label'] ?? $version['name'] ?? $version['version'] ?? $versionId; $label = $version['label'] ?? $version['name'] ?? $version['version'] ?? $versionId;
$description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null; $description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null;
$createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null); $createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null);
$figmaUserId = $version['user']['id'] ?? $version['user_id'] ?? null;
$data = [ $data = [
'file_id' => $fileId, 'file_id' => $fileId,
@ -192,6 +193,7 @@ class FigmaSyncService
'description' => is_scalar($description) ? (string) $description : null, 'description' => is_scalar($description) ? (string) $description : null,
'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'), 'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'),
'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null), 'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null),
'figma_user_id' => is_scalar($figmaUserId) ? (string) $figmaUserId : null,
'last_modified_figma' => $createdAt, 'last_modified_figma' => $createdAt,
'created_at_figma' => $createdAt, 'created_at_figma' => $createdAt,
]; ];
@ -242,16 +244,23 @@ class FigmaSyncService
{ {
$allItems = []; $allItems = [];
$seenSignatures = []; $seenSignatures = [];
$seenPageUrls = [];
$page = 1; $page = 1;
$limit = 100; $limit = 100;
$maxPages = 100; $maxPages = 100;
$nextPageUrl = null;
while ($page <= $maxPages) { while ($page <= $maxPages) {
if ($nextPageUrl !== null) {
$response = $this->request('GET', $nextPageUrl);
} else {
$response = $this->request('GET', $endpoint, [ $response = $this->request('GET', $endpoint, [
'page' => $page, 'page' => $page,
'limit' => $limit, 'limit' => $limit,
'per_page' => $limit, 'per_page' => $limit,
'page_size' => $limit,
]); ]);
}
if ($response['success'] === false) { if ($response['success'] === false) {
return $response; return $response;
@ -282,6 +291,16 @@ class FigmaSyncService
$allItems = array_merge($allItems, $newItems); $allItems = array_merge($allItems, $newItems);
$nextPageUrl = $this->extractNextPageUrl($response['data']);
if ($nextPageUrl !== null) {
if (isset($seenPageUrls[$nextPageUrl])) {
break;
}
$seenPageUrls[$nextPageUrl] = true;
$page++;
continue;
}
if (count($items) < $limit) { if (count($items) < $limit) {
break; break;
} }
@ -315,6 +334,20 @@ class FigmaSyncService
return []; return [];
} }
private function extractNextPageUrl($data): ?string
{
if (!is_array($data)) {
return null;
}
$nextPage = $data['pagination']['next_page'] ?? $data['next_page'] ?? null;
if (!is_string($nextPage) || trim($nextPage) === '') {
return null;
}
return str_replace(' ', '%20', trim($nextPage));
}
private function sortByDateDesc(array $items, array $dateKeys): array private function sortByDateDesc(array $items, array $dateKeys): array
{ {
usort($items, function (array $left, array $right) use ($dateKeys): int { usort($items, function (array $left, array $right) use ($dateKeys): int {
@ -368,11 +401,14 @@ class FigmaSyncService
private function request(string $method, string $endpoint, array $query = []): array private function request(string $method, string $endpoint, array $query = []): array
{ {
$url = $this->baseUrl . $endpoint; $isAbsolute = str_starts_with($endpoint, 'http://') || str_starts_with($endpoint, 'https://');
$url = $isAbsolute ? $endpoint : $this->baseUrl . $endpoint;
if (!empty($query)) { if (!empty($query)) {
$url .= '?' . http_build_query($query); $separator = str_contains($url, '?') ? '&' : '?';
$url .= $separator . http_build_query($query);
} }
$url = str_replace(' ', '%20', $url);
$client = \Config\Services::curlrequest(); $client = \Config\Services::curlrequest();
try { try {

View File

@ -18,6 +18,7 @@ class FigmaFileVersionsModel extends Model
'description', 'description',
'name', 'name',
'editor_type', 'editor_type',
'figma_user_id',
'last_modified_figma', 'last_modified_figma',
'created_at_figma', 'created_at_figma',
]; ];

View File

@ -80,6 +80,7 @@
<th>Description</th> <th>Description</th>
<th>Version</th> <th>Version</th>
<th>Editor</th> <th>Editor</th>
<th>Figma User ID</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -293,7 +294,7 @@ document.addEventListener('DOMContentLoaded', async () => {
async function loadVersions() { async function loadVersions() {
const tbody = document.querySelector('#tableVersions tbody'); const tbody = document.querySelector('#tableVersions tbody');
tbody.innerHTML = '<tr><td colspan="5">Loading...</td></tr>'; tbody.innerHTML = '<tr><td colspan="6">Loading...</td></tr>';
const response = await fetch(`<?= base_url('api/figma/snapshots') ?>?${getBaseParams(versionsState.page, versionsState.perPage)}`); const response = await fetch(`<?= base_url('api/figma/snapshots') ?>?${getBaseParams(versionsState.page, versionsState.perPage)}`);
const result = await response.json(); const result = await response.json();
@ -301,7 +302,7 @@ document.addEventListener('DOMContentLoaded', async () => {
versionsState.total = 0; versionsState.total = 0;
versionsState.totalPages = 0; versionsState.totalPages = 0;
totalVersionRows.innerText = '0'; totalVersionRows.innerText = '0';
tbody.innerHTML = '<tr><td colspan="5">Failed loading snapshots</td></tr>'; tbody.innerHTML = '<tr><td colspan="6">Failed loading snapshots</td></tr>';
updatePager(versionPrev, versionNext, versionPageInfo, versionsState); updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
return; return;
} }
@ -314,7 +315,7 @@ document.addEventListener('DOMContentLoaded', async () => {
totalVersionRows.innerText = String(versionsState.total); totalVersionRows.innerText = String(versionsState.total);
if (!result.data.length) { if (!result.data.length) {
tbody.innerHTML = '<tr><td colspan="5">No snapshots found</td></tr>'; tbody.innerHTML = '<tr><td colspan="6">No snapshots found</td></tr>';
updatePager(versionPrev, versionNext, versionPageInfo, versionsState); updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
return; return;
} }
@ -326,6 +327,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<td>${escapeHtml(item.description || '')}</td> <td>${escapeHtml(item.description || '')}</td>
<td>${escapeHtml(item.version || item.figma_version_id || '')}</td> <td>${escapeHtml(item.version || item.figma_version_id || '')}</td>
<td>${escapeHtml(item.editor_type || '')}</td> <td>${escapeHtml(item.editor_type || '')}</td>
<td>${escapeHtml(item.figma_user_id || '')}</td>
</tr>`; </tr>`;
}).join(''); }).join('');

205
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
"version": "v3.0.3", "version": "v3.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git", "url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
"reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -57,9 +57,9 @@
"homepage": "https://github.com/Bacon/BaconQrCode", "homepage": "https://github.com/Bacon/BaconQrCode",
"support": { "support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues", "issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1"
}, },
"time": "2025-11-19T17:15:36+00:00" "time": "2026-04-05T21:06:35+00:00"
}, },
{ {
"name": "brick/math", "name": "brick/math",
@ -123,36 +123,37 @@
}, },
{ {
"name": "codeigniter4/framework", "name": "codeigniter4/framework",
"version": "v4.6.3", "version": "v4.7.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/codeigniter4/framework.git", "url": "https://github.com/codeigniter4/framework.git",
"reference": "68d1a5896106f869452dd369a690dd5bc75160fb" "reference": "b3359be849be29394660c3aed909aa32b6c45cf6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/codeigniter4/framework/zipball/68d1a5896106f869452dd369a690dd5bc75160fb", "url": "https://api.github.com/repos/codeigniter4/framework/zipball/b3359be849be29394660c3aed909aa32b6c45cf6",
"reference": "68d1a5896106f869452dd369a690dd5bc75160fb", "reference": "b3359be849be29394660c3aed909aa32b6c45cf6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-intl": "*", "ext-intl": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"laminas/laminas-escaper": "^2.17", "laminas/laminas-escaper": "^2.18",
"php": "^8.1", "php": "^8.2",
"psr/log": "^3.0" "psr/log": "^3.0"
}, },
"require-dev": { "require-dev": {
"codeigniter/coding-standard": "^1.7", "codeigniter/coding-standard": "^1.7",
"fakerphp/faker": "^1.24", "fakerphp/faker": "^1.24",
"friendsofphp/php-cs-fixer": "^3.47.1", "friendsofphp/php-cs-fixer": "^3.47.1",
"kint-php/kint": "^6.0", "kint-php/kint": "^6.1",
"mikey179/vfsstream": "^1.6.12", "mikey179/vfsstream": "^1.6.12",
"nexusphp/cs-config": "^3.6", "nexusphp/cs-config": "^3.6",
"phpunit/phpunit": "^10.5.16 || ^11.2", "phpunit/phpunit": "^10.5.16 || ^11.2",
"predis/predis": "^3.0" "predis/predis": "^3.0"
}, },
"suggest": { "suggest": {
"ext-apcu": "If you use Cache class ApcuHandler",
"ext-curl": "If you use CURLRequest class", "ext-curl": "If you use CURLRequest class",
"ext-dom": "If you use TestResponse", "ext-dom": "If you use TestResponse",
"ext-exif": "If you run Image class tests", "ext-exif": "If you run Image class tests",
@ -164,7 +165,9 @@
"ext-memcached": "If you use Cache class MemcachedHandler with Memcached", "ext-memcached": "If you use Cache class MemcachedHandler with Memcached",
"ext-mysqli": "If you use MySQL", "ext-mysqli": "If you use MySQL",
"ext-oci8": "If you use Oracle Database", "ext-oci8": "If you use Oracle Database",
"ext-pcntl": "If you use Signals",
"ext-pgsql": "If you use PostgreSQL", "ext-pgsql": "If you use PostgreSQL",
"ext-posix": "If you use Signals",
"ext-readline": "Improves CLI::input() usability", "ext-readline": "Improves CLI::input() usability",
"ext-redis": "If you use Cache class RedisHandler", "ext-redis": "If you use Cache class RedisHandler",
"ext-simplexml": "If you format XML", "ext-simplexml": "If you format XML",
@ -193,7 +196,7 @@
"slack": "https://codeigniterchat.slack.com", "slack": "https://codeigniterchat.slack.com",
"source": "https://github.com/codeigniter4/CodeIgniter4" "source": "https://github.com/codeigniter4/CodeIgniter4"
}, },
"time": "2025-08-02T13:36:13+00:00" "time": "2026-03-24T18:26:09+00:00"
}, },
{ {
"name": "dasprid/enum", "name": "dasprid/enum",
@ -247,16 +250,16 @@
}, },
{ {
"name": "dompdf/dompdf", "name": "dompdf/dompdf",
"version": "v3.1.4", "version": "v3.1.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/dompdf/dompdf.git", "url": "https://github.com/dompdf/dompdf.git",
"reference": "db712c90c5b9868df3600e64e68da62e78a34623" "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623", "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"reference": "db712c90c5b9868df3600e64e68da62e78a34623", "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -305,9 +308,9 @@
"homepage": "https://github.com/dompdf/dompdf", "homepage": "https://github.com/dompdf/dompdf",
"support": { "support": {
"issues": "https://github.com/dompdf/dompdf/issues", "issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.4" "source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
}, },
"time": "2025-10-29T12:43:30+00:00" "time": "2026-03-03T13:54:37+00:00"
}, },
{ {
"name": "dompdf/php-font-lib", "name": "dompdf/php-font-lib",
@ -474,32 +477,32 @@
}, },
{ {
"name": "laminas/laminas-escaper", "name": "laminas/laminas-escaper",
"version": "2.17.0", "version": "2.18.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laminas/laminas-escaper.git", "url": "https://github.com/laminas/laminas-escaper.git",
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba" "reference": "06f211dfffff18d91844c1f55250d5d13c007e18"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/df1ef9503299a8e3920079a16263b578eaf7c3ba", "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18",
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba", "reference": "06f211dfffff18d91844c1f55250d5d13c007e18",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-ctype": "*", "ext-ctype": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
}, },
"conflict": { "conflict": {
"zendframework/zend-escaper": "*" "zendframework/zend-escaper": "*"
}, },
"require-dev": { "require-dev": {
"infection/infection": "^0.29.8", "infection/infection": "^0.31.0",
"laminas/laminas-coding-standard": "~3.0.1", "laminas/laminas-coding-standard": "~3.1.0",
"phpunit/phpunit": "^10.5.45", "phpunit/phpunit": "^11.5.42",
"psalm/plugin-phpunit": "^0.19.2", "psalm/plugin-phpunit": "^0.19.5",
"vimeo/psalm": "^6.6.2" "vimeo/psalm": "^6.13.1"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -531,7 +534,7 @@
"type": "community_bridge" "type": "community_bridge"
} }
], ],
"time": "2025-05-06T19:29:36+00:00" "time": "2025-10-14T18:31:13+00:00"
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
@ -806,33 +809,35 @@
}, },
{ {
"name": "sabberworm/php-css-parser", "name": "sabberworm/php-css-parser",
"version": "v9.1.0", "version": "v9.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb" "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-iconv": "*", "ext-iconv": "*",
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3" "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
}, },
"require-dev": { "require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0", "php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3", "phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.28 || 2.1.25", "phpstan/phpstan": "1.12.32 || 2.1.32",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
"phpunit/phpunit": "8.5.46", "phpunit/phpunit": "8.5.52",
"rawr/phpunit-data-provider": "3.3.1", "rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.1.7", "rector/rector": "1.2.10 || 2.2.8",
"rector/type-perfect": "1.0.0 || 2.1.0" "rector/type-perfect": "1.0.0 || 2.1.0",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
}, },
"suggest": { "suggest": {
"ext-mbstring": "for parsing UTF-8 CSS" "ext-mbstring": "for parsing UTF-8 CSS"
@ -840,10 +845,14 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "9.2.x-dev" "dev-main": "9.4.x-dev"
} }
}, },
"autoload": { "autoload": {
"files": [
"src/Rule/Rule.php",
"src/RuleSet/RuleContainer.php"
],
"psr-4": { "psr-4": {
"Sabberworm\\CSS\\": "src/" "Sabberworm\\CSS\\": "src/"
} }
@ -874,22 +883,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0" "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
}, },
"time": "2025-09-14T07:37:21+00:00" "time": "2026-03-03T17:31:43+00:00"
}, },
{ {
"name": "thecodingmachine/safe", "name": "thecodingmachine/safe",
"version": "v3.3.0", "version": "v3.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thecodingmachine/safe.git", "url": "https://github.com/thecodingmachine/safe.git",
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -999,7 +1008,7 @@
"description": "PHP core functions that throw exceptions instead of returning FALSE on error", "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": { "support": {
"issues": "https://github.com/thecodingmachine/safe/issues", "issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
}, },
"funding": [ "funding": [
{ {
@ -1010,12 +1019,16 @@
"url": "https://github.com/shish", "url": "https://github.com/shish",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/silasjoisten",
"type": "github"
},
{ {
"url": "https://github.com/staabm", "url": "https://github.com/staabm",
"type": "github" "type": "github"
} }
], ],
"time": "2025-05-14T06:15:44+00:00" "time": "2026-02-04T18:08:13+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -1196,16 +1209,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.6.1", "version": "v5.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1248,9 +1261,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
}, },
"time": "2025-08-13T20:13:15+00:00" "time": "2025-12-06T11:56:16+00:00"
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
@ -1693,16 +1706,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "10.5.51", "version": "10.5.63",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "ace160e31aaa317a99c411410c40c502b4be42a4" "reference": "33198268dad71e926626b618f3ec3966661e4d90"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ace160e31aaa317a99c411410c40c502b4be42a4", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90",
"reference": "ace160e31aaa317a99c411410c40c502b4be42a4", "reference": "33198268dad71e926626b618f3ec3966661e4d90",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1723,10 +1736,10 @@
"phpunit/php-timer": "^6.0.0", "phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1", "sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0", "sebastian/code-unit": "^2.0.0",
"sebastian/comparator": "^5.0.3", "sebastian/comparator": "^5.0.5",
"sebastian/diff": "^5.1.1", "sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0", "sebastian/environment": "^6.1.0",
"sebastian/exporter": "^5.1.2", "sebastian/exporter": "^5.1.4",
"sebastian/global-state": "^6.0.2", "sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0", "sebastian/object-enumerator": "^5.0.0",
"sebastian/recursion-context": "^5.0.1", "sebastian/recursion-context": "^5.0.1",
@ -1774,7 +1787,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.51" "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63"
}, },
"funding": [ "funding": [
{ {
@ -1798,7 +1811,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-12T07:31:25+00:00" "time": "2026-01-27T05:48:37+00:00"
}, },
{ {
"name": "psr/container", "name": "psr/container",
@ -2023,16 +2036,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "5.0.3", "version": "5.0.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2088,15 +2101,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5"
}, },
"funding": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
"type": "tidelift"
} }
], ],
"time": "2024-10-18T14:56:07+00:00" "time": "2026-01-24T09:25:16+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",
@ -2289,16 +2314,16 @@
}, },
{ {
"name": "sebastian/exporter", "name": "sebastian/exporter",
"version": "5.1.2", "version": "5.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git", "url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf" "reference": "0735b90f4da94969541dac1da743446e276defa6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf", "reference": "0735b90f4da94969541dac1da743446e276defa6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2307,7 +2332,7 @@
"sebastian/recursion-context": "^5.0" "sebastian/recursion-context": "^5.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.0" "phpunit/phpunit": "^10.5"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -2355,15 +2380,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues", "issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy", "security": "https://github.com/sebastianbergmann/exporter/security/policy",
"source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
}, },
"funding": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
"type": "tidelift"
} }
], ],
"time": "2024-03-02T07:17:12+00:00" "time": "2025-09-24T06:09:11+00:00"
}, },
{ {
"name": "sebastian/global-state", "name": "sebastian/global-state",
@ -2851,16 +2888,16 @@
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.2.3", "version": "1.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/tokenizer.git", "url": "https://github.com/theseer/tokenizer.git",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2889,7 +2926,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": { "support": {
"issues": "https://github.com/theseer/tokenizer/issues", "issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.3" "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
}, },
"funding": [ "funding": [
{ {
@ -2897,7 +2934,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-03-03T12:36:25+00:00" "time": "2025-11-17T20:03:58+00:00"
} }
], ],
"aliases": [], "aliases": [],

Binary file not shown.