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.
More information can be found at the [official site](https://codeigniter.com).
## Main modules
This repository holds a composer-installable app starter.
It has been built from the
[development repository](https://github.com/codeigniter4/CodeIgniter4).
- Auth and user management
- Dashboard
- 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/)
corresponding to the latest version of the framework.
## Installation & updates
`composer create-project codeigniter4/appstarter` then `composer update` whenever
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`.
- PHP 8.1+
- CodeIgniter 4
- MySQL/MariaDB
- dompdf for PDF output
- endroid/qr-code for QR generation
- ramsey/uuid for UUID support
## Setup
Copy `env` to `.env` and tailor for your app, specifically the baseURL
and any database settings.
1. Install dependencies:
## 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,
for better security and separation of components.
2. Copy `env` to `.env` and set app and database values:
This means that you should configure your web server to "point" to your project's *public* folder, and
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
framework are exposed.
```bash
copy env .env
```
**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.
We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss
FEATURE REQUESTS.
5. Run migrations if project uses them:
This repository is a "distribution" one, built by our release preparation script.
Problems with it can be raised on our forum, or as issues in the main repository.
```bash
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)
- [mbstring](http://php.net/manual/en/mbstring.installation.php)
## Notes
> [!WARNING]
> - The end of life date for PHP 7.4 was November 28, 2022.
> - The end of life date for PHP 8.0 was November 26, 2023.
> - 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
- `index.php` lives in `public/` for security.
- Check `app/Config/Routes.php` for module URLs and entry points.
- Use `app/Config/` and `.env` for environment-specific settings.

View File

@ -103,7 +103,7 @@ class FigmaApi extends BaseController
$total = (int) (clone $baseBuilder)->countAllResults();
$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')
->limit($perPage, $offset)
->get()
@ -177,7 +177,7 @@ class FigmaApi extends BaseController
}
$service = new FigmaSyncService();
$result = $service->syncAll();
$result = $service->syncIncremental(1);
$statusCode = $result['success'] ? 200 : 500;
return $this->respond([

View File

@ -28,6 +28,15 @@ class CreateFigmaTables extends Migration
'constraint' => 255,
'null' => true,
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'description' => [
'type' => 'LONGTEXT',
'null' => true,
],
'last_modified' => [
'type' => 'DATETIME',
'null' => true,
@ -75,6 +84,15 @@ class CreateFigmaTables extends Migration
'constraint' => 255,
'null' => true,
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'description' => [
'type' => 'LONGTEXT',
'null' => true,
],
'name' => [
'type' => 'VARCHAR',
'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;
$description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null;
$createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null);
$figmaUserId = $version['user']['id'] ?? $version['user_id'] ?? null;
$data = [
'file_id' => $fileId,
@ -192,6 +193,7 @@ class FigmaSyncService
'description' => is_scalar($description) ? (string) $description : null,
'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'),
'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null),
'figma_user_id' => is_scalar($figmaUserId) ? (string) $figmaUserId : null,
'last_modified_figma' => $createdAt,
'created_at_figma' => $createdAt,
];
@ -242,16 +244,23 @@ class FigmaSyncService
{
$allItems = [];
$seenSignatures = [];
$seenPageUrls = [];
$page = 1;
$limit = 100;
$maxPages = 100;
$nextPageUrl = null;
while ($page <= $maxPages) {
$response = $this->request('GET', $endpoint, [
'page' => $page,
'limit' => $limit,
'per_page' => $limit,
]);
if ($nextPageUrl !== null) {
$response = $this->request('GET', $nextPageUrl);
} else {
$response = $this->request('GET', $endpoint, [
'page' => $page,
'limit' => $limit,
'per_page' => $limit,
'page_size' => $limit,
]);
}
if ($response['success'] === false) {
return $response;
@ -282,6 +291,16 @@ class FigmaSyncService
$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) {
break;
}
@ -315,6 +334,20 @@ class FigmaSyncService
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
{
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
{
$url = $this->baseUrl . $endpoint;
$isAbsolute = str_starts_with($endpoint, 'http://') || str_starts_with($endpoint, 'https://');
$url = $isAbsolute ? $endpoint : $this->baseUrl . $endpoint;
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();
try {

View File

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

View File

@ -80,6 +80,7 @@
<th>Description</th>
<th>Version</th>
<th>Editor</th>
<th>Figma User ID</th>
</tr>
</thead>
<tbody></tbody>
@ -293,7 +294,7 @@ document.addEventListener('DOMContentLoaded', async () => {
async function loadVersions() {
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 result = await response.json();
@ -301,7 +302,7 @@ document.addEventListener('DOMContentLoaded', async () => {
versionsState.total = 0;
versionsState.totalPages = 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);
return;
}
@ -314,7 +315,7 @@ document.addEventListener('DOMContentLoaded', async () => {
totalVersionRows.innerText = String(versionsState.total);
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);
return;
}
@ -326,6 +327,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<td>${escapeHtml(item.description || '')}</td>
<td>${escapeHtml(item.version || item.figma_version_id || '')}</td>
<td>${escapeHtml(item.editor_type || '')}</td>
<td>${escapeHtml(item.figma_user_id || '')}</td>
</tr>`;
}).join('');

205
composer.lock generated
View File

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

Binary file not shown.