diff --git a/README.md b/README.md index d14b4c9..64d016c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/Controllers/Api/FigmaApi.php b/app/Controllers/Api/FigmaApi.php index a00b556..2d05c86 100644 --- a/app/Controllers/Api/FigmaApi.php +++ b/app/Controllers/Api/FigmaApi.php @@ -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([ diff --git a/app/Database/Migrations/2026-04-27-000001_CreateFigmaTables.php b/app/Database/Migrations/2026-04-27-000001_CreateFigmaTables.php index 3f4ed02..e8bfd19 100644 --- a/app/Database/Migrations/2026-04-27-000001_CreateFigmaTables.php +++ b/app/Database/Migrations/2026-04-27-000001_CreateFigmaTables.php @@ -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, diff --git a/app/Database/Migrations/2026-04-27-000002_AddLabelDescriptionToFigmaVersions.php b/app/Database/Migrations/2026-04-27-000002_AddLabelDescriptionToFigmaVersions.php deleted file mode 100644 index 274350b..0000000 --- a/app/Database/Migrations/2026-04-27-000002_AddLabelDescriptionToFigmaVersions.php +++ /dev/null @@ -1,43 +0,0 @@ -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'); - } - } -} diff --git a/app/Database/Migrations/2026-04-27-000003_RemoveFileUrlAndThumbnailFromFigmaTables.php b/app/Database/Migrations/2026-04-27-000003_RemoveFileUrlAndThumbnailFromFigmaTables.php deleted file mode 100644 index 85370c6..0000000 --- a/app/Database/Migrations/2026-04-27-000003_RemoveFileUrlAndThumbnailFromFigmaTables.php +++ /dev/null @@ -1,68 +0,0 @@ -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); - } - } -} diff --git a/app/Database/Migrations/2026-04-28-000001_AddFigmaUserIdToFileVersions.php b/app/Database/Migrations/2026-04-28-000001_AddFigmaUserIdToFileVersions.php new file mode 100644 index 0000000..f6f5025 --- /dev/null +++ b/app/Database/Migrations/2026-04-28-000001_AddFigmaUserIdToFileVersions.php @@ -0,0 +1,27 @@ +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'); + } +} diff --git a/app/Libraries/FigmaSyncService.php b/app/Libraries/FigmaSyncService.php index 29e9038..b51643b 100644 --- a/app/Libraries/FigmaSyncService.php +++ b/app/Libraries/FigmaSyncService.php @@ -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 { diff --git a/app/Models/FigmaFileVersionsModel.php b/app/Models/FigmaFileVersionsModel.php index 05977d9..13bc3d5 100644 --- a/app/Models/FigmaFileVersionsModel.php +++ b/app/Models/FigmaFileVersionsModel.php @@ -18,6 +18,7 @@ class FigmaFileVersionsModel extends Model 'description', 'name', 'editor_type', + 'figma_user_id', 'last_modified_figma', 'created_at_figma', ]; diff --git a/app/Views/figma_dashboard.php b/app/Views/figma_dashboard.php index d4e2fda..688ec64 100644 --- a/app/Views/figma_dashboard.php +++ b/app/Views/figma_dashboard.php @@ -80,6 +80,7 @@ Description Version Editor + Figma User ID @@ -293,7 +294,7 @@ document.addEventListener('DOMContentLoaded', async () => { async function loadVersions() { const tbody = document.querySelector('#tableVersions tbody'); - tbody.innerHTML = 'Loading...'; + tbody.innerHTML = 'Loading...'; const response = await fetch(`?${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 = 'Failed loading snapshots'; + tbody.innerHTML = 'Failed loading snapshots'; 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 = 'No snapshots found'; + tbody.innerHTML = 'No snapshots found'; updatePager(versionPrev, versionNext, versionPageInfo, versionsState); return; } @@ -326,6 +327,7 @@ document.addEventListener('DOMContentLoaded', async () => { ${escapeHtml(item.description || '')} ${escapeHtml(item.version || item.figma_version_id || '')} ${escapeHtml(item.editor_type || '')} + ${escapeHtml(item.figma_user_id || '')} `; }).join(''); diff --git a/composer.lock b/composer.lock index 0c8b312..bd4e7f2 100644 --- a/composer.lock +++ b/composer.lock @@ -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": [], diff --git a/crmv2_20260428_010106.sql.gz b/crmv2_20260428_010106.sql.gz new file mode 100644 index 0000000..53e828c Binary files /dev/null and b/crmv2_20260428_010106.sql.gz differ