diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php
index c618a82e0..1a273120c 100644
--- a/app/Controllers/Sales.php
+++ b/app/Controllers/Sales.php
@@ -4,9 +4,11 @@ namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Email_lib;
+use App\Libraries\InvoiceAttachment\InvoiceAttachmentGenerator;
use App\Libraries\Sale_lib;
use App\Libraries\Tax_lib;
use App\Libraries\Token_lib;
+use App\Libraries\UBLGenerator;
use App\Models\Customer;
use App\Models\Customer_rewards;
use App\Models\Dinner_table;
@@ -904,15 +906,28 @@ class Sales extends Secure_Controller
$text = $this->token_lib->render($text, $tokens);
$sale_data['mimetype'] = mime_content_type(FCPATH . 'uploads/' . $this->config['company_logo']);
- // Generate email attachment: invoice in PDF format
- $view = Services::renderer();
- $html = $view->setData($sale_data)->render("sales/$type" . '_email', $sale_data);
+ // Add config and customer object for attachment generation
+ $sale_data['config'] = $this->config;
+ $customer_id = $this->sale_lib->get_customer();
+ if ($customer_id && $customer_id != NEW_ENTRY) {
+ $sale_data['customer_object'] = $this->customer->get_info($customer_id);
+ }
- // Load PDF helper
- helper(['dompdf', 'file']);
- $filename = sys_get_temp_dir() . '/' . lang('Sales.' . $type) . '-' . str_replace('/', '-', $number) . '.pdf';
- if (file_put_contents($filename, create_pdf($html)) !== false) {
- $result = $this->email_lib->sendEmail($to, $subject, $text, $filename);
+ // Generate attachments based on config
+ $invoiceFormat = $this->config['invoice_format'] ?? 'pdf_only';
+ $attachmentGenerator = InvoiceAttachmentGenerator::createFromConfig($invoiceFormat);
+ $attachments = $attachmentGenerator->generateAttachments($sale_data, $type);
+
+ if (!empty($attachments)) {
+ try {
+ if (count($attachments) === 1) {
+ $result = $this->email_lib->sendEmail($to, $subject, $text, $attachments[0]);
+ } else {
+ $result = $this->email_lib->sendMultipleAttachments($to, $subject, $text, $attachments);
+ }
+ } finally {
+ InvoiceAttachmentGenerator::cleanup($attachments);
+ }
}
$message = lang($result ? "Sales." . $type . "_sent" : "Sales." . $type . "_unsent") . ' ' . $to;
@@ -1273,6 +1288,47 @@ class Sales extends Secure_Controller
return view('sales/' . $data['invoice_view'], $data);
}
+ /**
+ * Generate and download UBL invoice
+ *
+ * @param int $sale_id
+ * @return ResponseInterface
+ */
+ public function getUblInvoice($sale_id): ResponseInterface
+ {
+ $sale_info = $this->sale->get_info($sale_id)->getRowArray();
+
+ if (empty($sale_info) || empty($sale_info['invoice_number'])) {
+ return $this->response->setStatusCode(404)->setBody(lang('Sales.sale_not_found'));
+ }
+
+ try {
+ $sale_data = $this->_load_sale_data($sale_id);
+ $sale_data['config'] = $this->config;
+ $customer_id = $this->sale_lib->get_customer();
+ if ($customer_id && $customer_id != NEW_ENTRY) {
+ $sale_data['customer_object'] = $this->customer->get_info($customer_id);
+ }
+ $ublGenerator = new UBLGenerator();
+ $xml = $ublGenerator->generateUblInvoice($sale_data);
+
+ $rawFilename = lang('Sales.invoice') . '-' . str_replace('/', '-', $sale_data['invoice_number']) . '.xml';
+ $rawFilename = str_replace(["\r", "\n"], '', $rawFilename);
+ $safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', $rawFilename);
+ $contentDisposition = 'attachment; filename="' . $safeFilename . '"; filename*=UTF-8\'\'' . rawurlencode($rawFilename);
+
+ return $this->response
+ ->setHeader('Content-Type', 'application/xml')
+ ->setHeader('Content-Disposition', $contentDisposition)
+ ->setBody($xml);
+ } catch (\Exception $e) {
+ log_message('error', 'UBL generation failed: ' . $e->getMessage());
+ return $this->response->setStatusCode(500)->setBody(lang('Sales.ubl_generation_failed'));
+ } finally {
+ $this->sale_lib->clear_all();
+ }
+ }
+
/**
* Edits an existing sale or work order. Used in app/Views/sales/form.php
*
diff --git a/app/Database/Migrations/20260304000000_addUblConfig.php b/app/Database/Migrations/20260304000000_addUblConfig.php
new file mode 100644
index 000000000..9165f673a
--- /dev/null
+++ b/app/Database/Migrations/20260304000000_addUblConfig.php
@@ -0,0 +1,24 @@
+ 'invoice_format', 'value' => 'pdf_only']
+ ];
+
+ $this->db->table('app_config')->ignore(true)->insertBatch($config_values);
+ }
+
+ public function down(): void
+ {
+ $this->db->table('app_config')->whereIn('key', ['invoice_format'])->delete();
+ }
+}
\ No newline at end of file
diff --git a/app/Helpers/country_helper.php b/app/Helpers/country_helper.php
new file mode 100644
index 000000000..b56f21042
--- /dev/null
+++ b/app/Helpers/country_helper.php
@@ -0,0 +1,229 @@
+ 'BE',
+ 'Belgique' => 'BE',
+ 'België' => 'BE',
+ 'United States' => 'US',
+ 'USA' => 'US',
+ 'United States of America' => 'US',
+ 'United Kingdom' => 'GB',
+ 'UK' => 'GB',
+ 'Great Britain' => 'GB',
+ 'France' => 'FR',
+ 'Germany' => 'DE',
+ 'Deutschland' => 'DE',
+ 'Netherlands' => 'NL',
+ 'The Netherlands' => 'NL',
+ 'Nederland' => 'NL',
+ 'Italy' => 'IT',
+ 'Italia' => 'IT',
+ 'Spain' => 'ES',
+ 'España' => 'ES',
+ 'Poland' => 'PL',
+ 'Polska' => 'PL',
+ 'Portugal' => 'PT',
+ 'Sweden' => 'SE',
+ 'Sverige' => 'SE',
+ 'Norway' => 'NO',
+ 'Norge' => 'NO',
+ 'Denmark' => 'DK',
+ 'Danmark' => 'DK',
+ 'Finland' => 'FI',
+ 'Suomi' => 'FI',
+ 'Switzerland' => 'CH',
+ 'Suisse' => 'CH',
+ 'Schweiz' => 'CH',
+ 'Austria' => 'AT',
+ 'Österreich' => 'AT',
+ 'Ireland' => 'IE',
+ 'Luxembourg' => 'LU',
+ 'Greece' => 'GR',
+ 'Czech Republic' => 'CZ',
+ 'Czechia' => 'CZ',
+ 'Hungary' => 'HU',
+ 'Romania' => 'RO',
+ 'Bulgaria' => 'BG',
+ 'Slovakia' => 'SK',
+ 'Slovenia' => 'SI',
+ 'Estonia' => 'EE',
+ 'Latvia' => 'LV',
+ 'Lithuania' => 'LT',
+ 'Croatia' => 'HR',
+ 'Serbia' => 'RS',
+ 'Montenegro' => 'ME',
+ 'Bosnia and Herzegovina' => 'BA',
+ 'North Macedonia' => 'MK',
+ 'Albania' => 'AL',
+ 'Kosovo' => 'XK',
+ 'Turkey' => 'TR',
+ 'Türkiye' => 'TR',
+ 'Russia' => 'RU',
+ 'Russian Federation' => 'RU',
+ 'Ukraine' => 'UA',
+ 'Belarus' => 'BY',
+ 'Moldova' => 'MD',
+ 'Georgia' => 'GE',
+ 'Armenia' => 'AM',
+ 'Azerbaijan' => 'AZ',
+ 'Kazakhstan' => 'KZ',
+ 'Uzbekistan' => 'UZ',
+
+ // Other major economies
+ 'China' => 'CN',
+ 'Japan' => 'JP',
+ 'South Korea' => 'KR',
+ 'Korea' => 'KR',
+ 'India' => 'IN',
+ 'Australia' => 'AU',
+ 'New Zealand' => 'NZ',
+ 'Canada' => 'CA',
+ 'Mexico' => 'MX',
+ 'Brazil' => 'BR',
+ 'Argentina' => 'AR',
+ 'Chile' => 'CL',
+ 'Colombia' => 'CO',
+ 'Peru' => 'PE',
+ 'South Africa' => 'ZA',
+ 'Egypt' => 'EG',
+ 'Nigeria' => 'NG',
+ 'Kenya' => 'KE',
+ 'Morocco' => 'MA',
+
+ // If already ISO code, return as-is
+ 'BE' => 'BE',
+ 'US' => 'US',
+ 'GB' => 'GB',
+ 'FR' => 'FR',
+ 'DE' => 'DE',
+ 'NL' => 'NL',
+ 'IT' => 'IT',
+ 'ES' => 'ES',
+ 'PT' => 'PT',
+ 'SE' => 'SE',
+ 'NO' => 'NO',
+ 'DK' => 'DK',
+ 'FI' => 'FI',
+ 'CH' => 'CH',
+ 'AT' => 'AT',
+ 'IE' => 'IE',
+ 'LU' => 'LU',
+ 'GR' => 'GR',
+ 'CZ' => 'CZ',
+ 'HU' => 'HU',
+ 'RO' => 'RO',
+ 'BG' => 'BG',
+ 'SK' => 'SK',
+ 'SI' => 'SI',
+ 'EE' => 'EE',
+ 'LV' => 'LV',
+ 'LT' => 'LT',
+ 'HR' => 'HR',
+ 'RS' => 'RS',
+ 'ME' => 'ME',
+ 'BA' => 'BA',
+ 'MK' => 'MK',
+ 'AL' => 'AL',
+ 'TR' => 'TR',
+ 'RU' => 'RU',
+ 'UA' => 'UA',
+ ];
+
+ // Try exact match first
+ $normalized = trim($countryName);
+ if (isset($countryMap[$normalized])) {
+ return $countryMap[$normalized];
+ }
+
+ // Try case-insensitive match
+ $normalizedLower = strtolower($normalized);
+ foreach ($countryMap as $key => $code) {
+ if (strtolower($key) === $normalizedLower) {
+ return $code;
+ }
+ }
+
+ // Try partial match (e.g., "United States" → "US")
+ foreach ($countryMap as $key => $code) {
+ if (stripos($key, $normalized) !== false || stripos($normalized, $key) !== false) {
+ return $code;
+ }
+ }
+
+ // Try matching ISO code directly
+ if (preg_match('/^[A-Z]{2}$/i', $normalized)) {
+ return strtoupper($normalized);
+ }
+
+ // Check if the country_codes config has a default
+ $config = config(OSPOS::class)->settings;
+ if (isset($config['country_codes']) && !empty($config['country_codes'])) {
+ $countries = explode(',', $config['country_codes']);
+ if (!empty($countries)) {
+ return strtoupper(trim($countries[0]));
+ }
+ }
+
+ // Default to Belgium (for Peppol compliance in Belgium)
+ return 'BE';
+ }
+}
+
+if (!function_exists('getCurrencyCode')) {
+ /**
+ * Get ISO 4217 currency code for a country
+ *
+ * @param string $countryCode ISO 3166-1 alpha-2 country code
+ * @return string ISO 4217 currency code
+ */
+ function getCurrencyCode(string $countryCode): string
+ {
+ $currencyMap = [
+ 'BE' => 'EUR',
+ 'FR' => 'EUR',
+ 'DE' => 'EUR',
+ 'NL' => 'EUR',
+ 'IT' => 'EUR',
+ 'ES' => 'EUR',
+ 'PT' => 'EUR',
+ 'IE' => 'EUR',
+ 'AT' => 'EUR',
+ 'LU' => 'EUR',
+ 'FI' => 'EUR',
+ 'GR' => 'EUR',
+ 'US' => 'USD',
+ 'GB' => 'GBP',
+ 'CH' => 'CHF',
+ 'JP' => 'JPY',
+ 'CN' => 'CNY',
+ 'CA' => 'CAD',
+ 'AU' => 'AUD',
+ 'NZ' => 'NZD',
+ 'IN' => 'INR',
+ 'BR' => 'BRL',
+ 'MX' => 'MXN',
+ 'ZA' => 'ZAR',
+ ];
+
+ return $currencyMap[$countryCode] ?? 'EUR'; // Default to EUR
+ }
+}
\ No newline at end of file
diff --git a/app/Helpers/tabular_helper.php b/app/Helpers/tabular_helper.php
index 70effeba7..d5d81a839 100644
--- a/app/Helpers/tabular_helper.php
+++ b/app/Helpers/tabular_helper.php
@@ -84,6 +84,7 @@ function get_sales_manage_table_headers(): string
if ($config['invoice_enable']) {
$headers[] = ['invoice_number' => lang('Sales.invoice_number')];
$headers[] = ['invoice' => '', 'sortable' => false, 'escape' => false];
+ $headers[] = ['ubl' => '', 'sortable' => false, 'escape' => false];
}
$headers[] = ['receipt' => '', 'sortable' => false, 'escape' => false];
@@ -120,6 +121,13 @@ function get_sale_data_row(object $sale): array
'',
['title' => lang('Sales.show_invoice')]
);
+ $row['ubl'] = empty($sale->invoice_number)
+ ? '-'
+ : anchor(
+ "$controller/ublInvoice/$sale->sale_id",
+ '',
+ ['title' => lang('Sales.download_ubl'), 'target' => '_blank']
+ );
}
$row['receipt'] = anchor(
@@ -654,7 +662,7 @@ function expand_attribute_values(array $definition_names, array $row): array
foreach ($definition_names as $definition_id => $definitionInfo) {
if (isset($indexed_values[$definition_id])) {
$raw_value = $indexed_values[$definition_id];
-
+
// Format DECIMAL attributes according to locale
if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) {
$attribute_values["$definition_id"] = to_decimals($raw_value);
@@ -934,7 +942,7 @@ function get_controller(): string
/**
* Restores filter values from URL query string.
- *
+ *
* @param CodeIgniter\HTTP\IncomingRequest $request The request object
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
*/
@@ -943,7 +951,7 @@ function restoreTableFilters($request): array
$startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
-
+
return array_filter([
'start_date' => $startDate ?: null,
'end_date' => $endDate ?: null,
diff --git a/app/Language/ar-EG/Sales.php b/app/Language/ar-EG/Sales.php
index bf8d5b7cd..330c9ffb7 100644
--- a/app/Language/ar-EG/Sales.php
+++ b/app/Language/ar-EG/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "رقم طلب العمل يجب ان يكون فريد.",
"work_order_sent" => "تم ارسال طلب العمل الى",
"work_order_unsent" => "فشل في ارسال طلب العمل الى",
+ "sale_not_found" => "البيع غير موجود",
];
diff --git a/app/Language/ar-LB/Sales.php b/app/Language/ar-LB/Sales.php
index 112838301..bf36afea2 100644
--- a/app/Language/ar-LB/Sales.php
+++ b/app/Language/ar-LB/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "رقم طلب العمل يجب ان يكون فريد.",
"work_order_sent" => "تم ارسال طلب العمل الى",
"work_order_unsent" => "فشل في ارسال طلب العمل الى",
+ "sale_not_found" => "البيع غير موجود",
];
diff --git a/app/Language/az/Sales.php b/app/Language/az/Sales.php
index 4ab9586c9..95ece31ba 100644
--- a/app/Language/az/Sales.php
+++ b/app/Language/az/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "İş sifarişinin nömrəsi unikal olmalıdır.",
"work_order_sent" => "İş sifarişi göndərildi",
"work_order_unsent" => "İş Sifarişi göndərilməmişdi",
+ "sale_not_found" => "Satış tapılmadı",
];
diff --git a/app/Language/bg/Sales.php b/app/Language/bg/Sales.php
index 43293ef6f..35ec420be 100644
--- a/app/Language/bg/Sales.php
+++ b/app/Language/bg/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Номерът на работната поръчка трябва да е уникален.",
"work_order_sent" => "Работната поръчка е изпратена до",
"work_order_unsent" => "Работната поръчка не бе изпратена до",
+ "sale_not_found" => "Продажбата не е намерена",
];
diff --git a/app/Language/bs/Sales.php b/app/Language/bs/Sales.php
index f623af079..a1b3ec84e 100644
--- a/app/Language/bs/Sales.php
+++ b/app/Language/bs/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Broj radnog naloga mora biti jedinstven.",
"work_order_sent" => "Radni nalog poslat na",
"work_order_unsent" => "Slanje radnog naloga nije uspjelo",
+ "sale_not_found" => "Prodaja nije pronađena",
];
diff --git a/app/Language/ckb/Sales.php b/app/Language/ckb/Sales.php
index 7f5d46dc2..d7b93080d 100644
--- a/app/Language/ckb/Sales.php
+++ b/app/Language/ckb/Sales.php
@@ -223,4 +223,5 @@ return [
'work_order_number_duplicate' => "ژمارەی داواکاری کار دەبێت تایبەت بێت.",
'work_order_sent' => "داواکاری کار نێردرا بۆ",
'work_order_unsent' => "داواکاری کار شکستی هێنا لە ناردنی بۆ",
+ 'sale_not_found' => "فرۆش نەدۆزرایەوە",
];
diff --git a/app/Language/cs/Sales.php b/app/Language/cs/Sales.php
index 623446543..418d8d8ad 100644
--- a/app/Language/cs/Sales.php
+++ b/app/Language/cs/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Prodej nenalezen",
];
diff --git a/app/Language/da/Sales.php b/app/Language/da/Sales.php
index ee07a62eb..a4e50fcaf 100644
--- a/app/Language/da/Sales.php
+++ b/app/Language/da/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Salg ikke fundet",
];
diff --git a/app/Language/de-CH/Sales.php b/app/Language/de-CH/Sales.php
index 32d1cc228..018beb709 100644
--- a/app/Language/de-CH/Sales.php
+++ b/app/Language/de-CH/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Verkauf nicht gefunden",
];
diff --git a/app/Language/el/Sales.php b/app/Language/el/Sales.php
index 90dd0d5d0..d11b03c8f 100644
--- a/app/Language/el/Sales.php
+++ b/app/Language/el/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Ο Αριθμός της Παραγγελίας Εργασίας πρέπει να είναι μοναδικός.",
"work_order_sent" => "Εντολή Εργασίας απεστάλη σε",
"work_order_unsent" => "Ανεπιτυχής αποστολή Εντολής Εργασίας",
+ "sale_not_found" => "Η πώληση δεν βρέθηκε",
];
diff --git a/app/Language/en-GB/Sales.php b/app/Language/en-GB/Sales.php
index d9678ba44..bb2cc7947 100644
--- a/app/Language/en-GB/Sales.php
+++ b/app/Language/en-GB/Sales.php
@@ -223,4 +223,5 @@ return [
"work_order_number_duplicate" => "Work Order Number must be unique.",
"work_order_sent" => "Work Order sent to",
"work_order_unsent" => "Work Order failed to be sent to",
+ "sale_not_found" => "Sale not found",
];
diff --git a/app/Language/en/Sales.php b/app/Language/en/Sales.php
index 6c7d93d48..0249d656e 100644
--- a/app/Language/en/Sales.php
+++ b/app/Language/en/Sales.php
@@ -223,4 +223,8 @@ return [
"work_order_number_duplicate" => "Work Order Number must be unique.",
"work_order_sent" => "Work Order sent to",
"work_order_unsent" => "Work Order failed to be sent to",
+ "ubl_invoice" => "UBL Invoice",
+ "download_ubl" => "Download UBL Invoice",
+ "ubl_generation_failed" => "Failed to generate UBL invoice",
+ "sale_not_found" => "Sale not found",
];
diff --git a/app/Language/es-MX/Sales.php b/app/Language/es-MX/Sales.php
index b0e521c5b..837ed3faa 100644
--- a/app/Language/es-MX/Sales.php
+++ b/app/Language/es-MX/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "El número de orden de trabajo debe ser único.",
"work_order_sent" => "Orden de trabajo enviada a",
"work_order_unsent" => "Falló la Orden de Trabajo al enviar a",
+ "sale_not_found" => "Venta no encontrada",
];
diff --git a/app/Language/fa/Sales.php b/app/Language/fa/Sales.php
index 182a534e2..2479b5eef 100644
--- a/app/Language/fa/Sales.php
+++ b/app/Language/fa/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "شماره سفارش کار باید منحصر به فرد باشد.",
"work_order_sent" => "دستور کار ارسال شده به",
"work_order_unsent" => "دستور کار نتوانست به ارسال شود",
+ "sale_not_found" => "فروش یافت نشد",
];
diff --git a/app/Language/he/Sales.php b/app/Language/he/Sales.php
index f2bd399ba..07d909464 100644
--- a/app/Language/he/Sales.php
+++ b/app/Language/he/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "מספר הזמנת עבודה חייב להיות ייחודי.",
"work_order_sent" => "הזמנת עבודה נשלחה אל",
"work_order_unsent" => "הזמנת עבודה לא נשלחה אל",
+ "sale_not_found" => "המכירה לא נמצאה",
];
diff --git a/app/Language/hr-HR/Sales.php b/app/Language/hr-HR/Sales.php
index 9e2df3b99..64222c590 100644
--- a/app/Language/hr-HR/Sales.php
+++ b/app/Language/hr-HR/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Prodaja nije pronađena",
];
diff --git a/app/Language/hu/Sales.php b/app/Language/hu/Sales.php
index 5928b2708..e60ea7ab9 100644
--- a/app/Language/hu/Sales.php
+++ b/app/Language/hu/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Értékesítés nem található",
];
diff --git a/app/Language/hy/Sales.php b/app/Language/hy/Sales.php
index b38f57446..183a36f3d 100644
--- a/app/Language/hy/Sales.php
+++ b/app/Language/hy/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Վաճառքը գտնված չէ",
];
diff --git a/app/Language/id/Sales.php b/app/Language/id/Sales.php
index e15191351..4cb36bdc8 100644
--- a/app/Language/id/Sales.php
+++ b/app/Language/id/Sales.php
@@ -222,5 +222,6 @@ return [
"work_order_number_duplicate" => "Nomor Perintah Kerja tidak boleh sama.",
"work_order_sent" => "Perintah Kerja dikirim ke",
"work_order_unsent" => "Perintah Kerja gagal dikirim ke",
+ "sale_not_found" => "Penjualan tidak ditemukan",
"selected_customer" => "Pelanggan Terpilih",
];
diff --git a/app/Language/lo/Sales.php b/app/Language/lo/Sales.php
index 66745059f..6d9a5f561 100644
--- a/app/Language/lo/Sales.php
+++ b/app/Language/lo/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Work Order Number must be unique.",
"work_order_sent" => "Work Order sent to",
"work_order_unsent" => "Work Order failed to be sent to",
+ "sale_not_found" => "Sale not found",
];
diff --git a/app/Language/nb/Sales.php b/app/Language/nb/Sales.php
index b38f57446..17deba1a0 100644
--- a/app/Language/nb/Sales.php
+++ b/app/Language/nb/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Salg ikke funnet",
];
diff --git a/app/Language/nl-BE/Sales.php b/app/Language/nl-BE/Sales.php
index f2768e8ed..c002a2678 100644
--- a/app/Language/nl-BE/Sales.php
+++ b/app/Language/nl-BE/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Werkorder Nummer moet uniek zijn.",
"work_order_sent" => "Werkorder verzonder naar",
"work_order_unsent" => "Werkorder kon niet verzonden worden naar",
+ "sale_not_found" => "Verkoop niet gevonden",
];
diff --git a/app/Language/ro/Sales.php b/app/Language/ro/Sales.php
index 815b1176d..711076cb6 100644
--- a/app/Language/ro/Sales.php
+++ b/app/Language/ro/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "Vânzarea nu a fost găsită",
];
diff --git a/app/Language/ru/Sales.php b/app/Language/ru/Sales.php
index 7a3c2a977..f1bbcc947 100644
--- a/app/Language/ru/Sales.php
+++ b/app/Language/ru/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Номер рабочего заказа должен быть уникальным.",
"work_order_sent" => "Рабочий заказ отправлен",
"work_order_unsent" => "Не удалось отправить рабочий заказ",
+ "sale_not_found" => "Продажа не найдена",
];
diff --git a/app/Language/sv/Sales.php b/app/Language/sv/Sales.php
index b492a389f..c7e30258e 100644
--- a/app/Language/sv/Sales.php
+++ b/app/Language/sv/Sales.php
@@ -222,5 +222,6 @@ return [
"work_order_number_duplicate" => "Arbetsorder nummer måste vara unikt.",
"work_order_sent" => "Arbetsorder skickad till",
"work_order_unsent" => "Arbetsorder gick ej att skicka till",
+ "sale_not_found" => "Försäljning hittades inte",
"selected_customer" => "Vald kund",
];
diff --git a/app/Language/sw-KE/Sales.php b/app/Language/sw-KE/Sales.php
index 2dc789bda..44ada4399 100644
--- a/app/Language/sw-KE/Sales.php
+++ b/app/Language/sw-KE/Sales.php
@@ -224,4 +224,5 @@ return [
"work_order_number_duplicate" => "Nambari ya Agizo la Kazi lazima iwe ya kipekee.",
"work_order_sent" => "Agizo la Kazi limetumwa kwa",
"work_order_unsent" => "Imeshindikana kutuma Agizo la Kazi kwa",
+ "sale_not_found" => "Mauzo hayapatikani",
];
\ No newline at end of file
diff --git a/app/Language/sw-TZ/Sales.php b/app/Language/sw-TZ/Sales.php
index 2dc789bda..44ada4399 100644
--- a/app/Language/sw-TZ/Sales.php
+++ b/app/Language/sw-TZ/Sales.php
@@ -224,4 +224,5 @@ return [
"work_order_number_duplicate" => "Nambari ya Agizo la Kazi lazima iwe ya kipekee.",
"work_order_sent" => "Agizo la Kazi limetumwa kwa",
"work_order_unsent" => "Imeshindikana kutuma Agizo la Kazi kwa",
+ "sale_not_found" => "Mauzo hayapatikani",
];
\ No newline at end of file
diff --git a/app/Language/th/Sales.php b/app/Language/th/Sales.php
index 0a8ec17b1..32dc0924a 100644
--- a/app/Language/th/Sales.php
+++ b/app/Language/th/Sales.php
@@ -222,5 +222,6 @@ return [
"work_order_number_duplicate" => "หมายเลขคำสั่งงานต้องไม่ซ้ำกัน",
"work_order_sent" => "คำสั่งงานส่งถึง",
"work_order_unsent" => "ส่งคำสั่งงานล้มเหลว",
+ "sale_not_found" => "ไม่พบการขาย",
"selected_customer" => "ลูกค้าที่เลือก",
];
diff --git a/app/Language/tl/Sales.php b/app/Language/tl/Sales.php
index 3cb4d1653..fc1b00dd6 100644
--- a/app/Language/tl/Sales.php
+++ b/app/Language/tl/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Work Order Number must be unique.",
"work_order_sent" => "Work Order sent to",
"work_order_unsent" => "Work Order failed to be sent to",
+ "sale_not_found" => "Sale not found",
];
diff --git a/app/Language/tr/Sales.php b/app/Language/tr/Sales.php
index 8901fa02f..56f104dac 100644
--- a/app/Language/tr/Sales.php
+++ b/app/Language/tr/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "İş Emri Numarası diğerinden farklı olmalı.",
"work_order_sent" => "İş Emri gönderildi:",
"work_order_unsent" => "İş Emri gönderilemedi:",
+ "sale_not_found" => "Satış bulunamadı",
];
diff --git a/app/Language/uk/Sales.php b/app/Language/uk/Sales.php
index 2524d3a3d..cc266a2db 100644
--- a/app/Language/uk/Sales.php
+++ b/app/Language/uk/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Такий номер робочого замовлення уже існує.",
"work_order_sent" => "Замовлення відправлено",
"work_order_unsent" => "Не вдалось отримати робоче замовлення",
+ "sale_not_found" => "Продаж не знайдено",
];
diff --git a/app/Language/vi/Sales.php b/app/Language/vi/Sales.php
index d99083c28..79ecf2dfd 100644
--- a/app/Language/vi/Sales.php
+++ b/app/Language/vi/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "Số giấy giao việc phải là duy nhất.",
"work_order_sent" => "Gửi Giấy giao việc cho",
"work_order_unsent" => "Gặp lỗi khi gửi Giấy giao việc cho",
+ "sale_not_found" => "Không tìm thấy giao dịch",
];
diff --git a/app/Language/zh-Hans/Sales.php b/app/Language/zh-Hans/Sales.php
index 4e04d3d09..b15f00b2a 100644
--- a/app/Language/zh-Hans/Sales.php
+++ b/app/Language/zh-Hans/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "",
"work_order_sent" => "",
"work_order_unsent" => "",
+ "sale_not_found" => "找不到销售",
];
diff --git a/app/Language/zh-Hant/Sales.php b/app/Language/zh-Hant/Sales.php
index cb1af0dfb..7c5327c7b 100644
--- a/app/Language/zh-Hant/Sales.php
+++ b/app/Language/zh-Hant/Sales.php
@@ -222,4 +222,5 @@ return [
"work_order_number_duplicate" => "工作編號重複.",
"work_order_sent" => "發送工作指示",
"work_order_unsent" => "工作指示發送失敗",
+ "sale_not_found" => "找不到銷售",
];
diff --git a/app/Libraries/Email_lib.php b/app/Libraries/Email_lib.php
index 914344567..607ffd523 100644
--- a/app/Libraries/Email_lib.php
+++ b/app/Libraries/Email_lib.php
@@ -82,4 +82,37 @@ class Email_lib
return $result;
}
+
+ /**
+ * Send email with multiple attachments
+ *
+ * @param string $to
+ * @param string $subject
+ * @param string $message
+ * @param array $attachments
+ * @return bool
+ */
+ public function sendMultipleAttachments(string $to, string $subject, string $message, array $attachments): bool
+ {
+ $email = $this->email;
+
+ $email->setFrom($this->config['email'], $this->config['company']);
+ $email->setTo($to);
+ $email->setSubject($subject);
+ $email->setMessage($message);
+
+ foreach ($attachments as $attachment) {
+ if (!empty($attachment) && file_exists($attachment)) {
+ $email->attach($attachment);
+ }
+ }
+
+ $result = $email->send();
+
+ if (!$result) {
+ log_message('error', $email->printDebugger());
+ }
+
+ return $result;
+ }
}
diff --git a/app/Libraries/InvoiceAttachment/InvoiceAttachment.php b/app/Libraries/InvoiceAttachment/InvoiceAttachment.php
new file mode 100644
index 000000000..d73edcdda
--- /dev/null
+++ b/app/Libraries/InvoiceAttachment/InvoiceAttachment.php
@@ -0,0 +1,40 @@
+attachments[] = $attachment;
+ return $this;
+ }
+
+ /**
+ * Create generator with attachments based on config.
+ * Factory method that instantiates the right attachments.
+ *
+ * @param string $invoiceFormat Config value: 'pdf_only', 'ubl_only', or 'both'
+ * @return self
+ */
+ public static function createFromConfig(string $invoiceFormat): self
+ {
+ $generator = new self();
+
+ if (in_array($invoiceFormat, ['pdf_only', 'both'], true)) {
+ $generator->register(new PdfAttachment());
+ }
+
+ if (in_array($invoiceFormat, ['ubl_only', 'both'], true)) {
+ $generator->register(new UblAttachment());
+ }
+
+ return $generator;
+ }
+
+ /**
+ * Generate all applicable attachments for a sale.
+ *
+ * @param array $saleData The sale data
+ * @param string $type The document type
+ * @return string[] Array of file paths to generated attachments
+ */
+ public function generateAttachments(array $saleData, string $type): array
+ {
+ $files = [];
+
+ foreach ($this->attachments as $attachment) {
+ if ($attachment->isApplicableForType($type, $saleData)) {
+ $filepath = $attachment->generate($saleData, $type);
+ if ($filepath !== null) {
+ $files[] = $filepath;
+ }
+ }
+ }
+
+ return $files;
+ }
+
+ /**
+ * Clean up temporary attachment files.
+ *
+ * @param string[] $files
+ */
+ public static function cleanup(array $files): void
+ {
+ foreach ($files as $file) {
+ @unlink($file);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Libraries/InvoiceAttachment/PdfAttachment.php b/app/Libraries/InvoiceAttachment/PdfAttachment.php
new file mode 100644
index 000000000..686fd16cd
--- /dev/null
+++ b/app/Libraries/InvoiceAttachment/PdfAttachment.php
@@ -0,0 +1,61 @@
+setData($saleData)->render("sales/{$type}_email");
+
+ helper(['dompdf', 'file']);
+
+ $tempPath = tempnam(sys_get_temp_dir(), 'ospos_pdf_');
+ if ($tempPath === false) {
+ log_message('error', 'PDF attachment: failed to create temp file');
+ return null;
+ }
+
+ $filename = $tempPath . '.pdf';
+ rename($tempPath, $filename);
+
+ $pdfContent = create_pdf($html);
+ if (file_put_contents($filename, $pdfContent) === false) {
+ log_message('error', 'PDF attachment: failed to write content');
+ @unlink($filename);
+ return null;
+ }
+
+ return $filename;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isApplicableForType(string $type, array $saleData): bool
+ {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getFileExtension(): string
+ {
+ return 'pdf';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getEnabledConfigValues(): array
+ {
+ return ['pdf_only', 'both'];
+ }
+}
\ No newline at end of file
diff --git a/app/Libraries/InvoiceAttachment/UblAttachment.php b/app/Libraries/InvoiceAttachment/UblAttachment.php
new file mode 100644
index 000000000..369cd692a
--- /dev/null
+++ b/app/Libraries/InvoiceAttachment/UblAttachment.php
@@ -0,0 +1,69 @@
+generateUblInvoice($saleData);
+
+ $tempPath = tempnam(sys_get_temp_dir(), 'ospos_ubl_');
+ if ($tempPath === false) {
+ log_message('error', 'UBL attachment: failed to create temp file');
+ return null;
+ }
+
+ $filename = $tempPath . '.xml';
+ rename($tempPath, $filename);
+
+ if (file_put_contents($filename, $xml) === false) {
+ log_message('error', 'UBL attachment: failed to write content');
+ @unlink($filename);
+ return null;
+ }
+
+ return $filename;
+ } catch (\Exception $e) {
+ log_message('error', 'UBL attachment generation failed: ' . $e->getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isApplicableForType(string $type, array $saleData): bool
+ {
+ return in_array($type, ['invoice', 'tax_invoice'], true)
+ && !empty($saleData['invoice_number']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getFileExtension(): string
+ {
+ return 'xml';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getEnabledConfigValues(): array
+ {
+ return ['ubl_only', 'both'];
+ }
+}
\ No newline at end of file
diff --git a/app/Libraries/UBLGenerator.php b/app/Libraries/UBLGenerator.php
new file mode 100644
index 000000000..e268853de
--- /dev/null
+++ b/app/Libraries/UBLGenerator.php
@@ -0,0 +1,243 @@
+setId('VAT');
+ $supplierParty = $this->buildSupplierParty($saleData, $taxScheme);
+ $customerParty = $this->buildCustomerParty($saleData['customer_object'] ?? null, $taxScheme);
+ $invoiceLines = $this->buildInvoiceLines($saleData['cart'], $taxScheme);
+ $taxTotal = $this->buildTaxTotal($saleData['taxes'], $taxScheme);
+ $monetaryTotal = $this->buildMonetaryTotal($saleData);
+
+ $invoice = (new Invoice())
+ ->setUBLVersionId('2.1')
+ ->setCustomizationId('urn:cen.eu:en16931:2017')
+ ->setProfileId('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0')
+ ->setId($saleData['invoice_number'])
+ ->setIssueDate(new \DateTime($saleData['transaction_date']))
+ ->setInvoiceTypeCode(380)
+ ->setAccountingSupplierParty($supplierParty)
+ ->setAccountingCustomerParty($customerParty)
+ ->setInvoiceLines($invoiceLines)
+ ->setTaxTotal($taxTotal)
+ ->setLegalMonetaryTotal($monetaryTotal);
+
+ $generator = new Generator();
+ return $generator->invoice($invoice);
+ }
+
+ protected function buildSupplierParty(array $saleData, TaxScheme $taxScheme): AccountingParty
+ {
+ $config = $saleData['config'];
+
+ $addressParts = $this->parseAddress($config['address'] ?? '');
+ $countryCode = 'BE'; // Default
+
+ $country = (new Country())->setIdentificationCode($countryCode);
+ $address = (new Address())
+ ->setStreetName($addressParts['street'] ?? '')
+ ->setBuildingNumber($addressParts['number'] ?? '')
+ ->setCityName($addressParts['city'] ?? '')
+ ->setPostalZone($addressParts['zip'] ?? '')
+ ->setCountrySubentity($config['state'] ?? '')
+ ->setCountry($country);
+
+ $party = (new Party())
+ ->setName($config['company'])
+ ->setPostalAddress($address);
+
+ if (!empty($config['account_number'])) {
+ $partyTaxScheme = (new PartyTaxScheme())
+ ->setCompanyId($config['account_number'])
+ ->setTaxScheme($taxScheme);
+ $party->setPartyTaxScheme($partyTaxScheme);
+ }
+
+ $accountingParty = (new AccountingParty())->setParty($party);
+
+ return $accountingParty;
+ }
+
+ protected function buildCustomerParty(?object $customerInfo, TaxScheme $taxScheme): AccountingParty
+ {
+ if ($customerInfo === null) {
+ return (new AccountingParty())->setParty(new Party());
+ }
+
+ $countryCode = getCountryCode($customerInfo->country ?? '');
+
+ $country = (new Country())->setIdentificationCode($countryCode);
+ $address = (new Address())
+ ->setStreetName($customerInfo->address_1 ?? '')
+ ->setAddressLine([$customerInfo->address_2 ?? ''])
+ ->setCityName($customerInfo->city ?? '')
+ ->setPostalZone($customerInfo->zip ?? '')
+ ->setCountrySubentity($customerInfo->state ?? '')
+ ->setCountry($country);
+
+ $partyName = !empty($customerInfo->company_name)
+ ? $customerInfo->company_name
+ : trim($customerInfo->first_name . ' ' . $customerInfo->last_name);
+
+ $party = (new Party())
+ ->setName($partyName)
+ ->setPostalAddress($address);
+
+ if (!empty($customerInfo->email)) {
+ $contact = (new Contact())
+ ->setElectronicMail($customerInfo->email)
+ ->setTelephone($customerInfo->phone_number ?? '');
+ $party->setContact($contact);
+ }
+
+ if (!empty($customerInfo->account_number)) {
+ $accountingParty = (new AccountingParty())
+ ->setParty($party)
+ ->setSupplierAssignedAccountId($customerInfo->account_number);
+ } else {
+ $accountingParty = (new AccountingParty())->setParty($party);
+ }
+
+ if (!empty($customerInfo->tax_id)) {
+ $partyTaxScheme = (new PartyTaxScheme())
+ ->setCompanyId($customerInfo->tax_id)
+ ->setTaxScheme($taxScheme);
+ $party->setPartyTaxScheme($partyTaxScheme);
+ }
+
+ return $accountingParty;
+ }
+
+ protected function buildInvoiceLines(array $cart, TaxScheme $taxScheme): array
+ {
+ $lines = [];
+ foreach ($cart as $item) {
+ $price = (new Price())
+ ->setBaseQuantity(1.0)
+ ->setUnitCode(UnitCode::UNIT)
+ ->setPriceAmount($item['price'] ?? 0);
+
+ $taxCategory = (new TaxCategory())
+ ->setId('S')
+ ->setPercent((float)($item['tax_rate'] ?? 0))
+ ->setTaxScheme($taxScheme);
+
+ $itemObj = (new Item())
+ ->setName($item['name'] ?? '')
+ ->setDescription($item['description'] ?? '');
+ $line = (new InvoiceLine())
+ ->setId(isset($item['line']) ? (string)$item['line'] : '1')
+ ->setItem($itemObj)
+ ->setPrice($price)
+ ->setInvoicedQuantity($item['quantity'] ?? 0);
+
+ $lines[] = $line;
+ }
+ return $lines;
+ }
+
+ protected function buildTaxTotal(array $taxes, TaxScheme $taxScheme): TaxTotal
+ {
+ $totalTax = '0';
+ $taxSubTotals = [];
+
+ foreach ($taxes as $tax) {
+ if (isset($tax['tax_rate'])) {
+ $taxRate = (string)$tax['tax_rate'];
+ $taxAmount = (string)($tax['sale_tax_amount'] ?? 0);
+
+ $taxCategory = (new TaxCategory())
+ ->setId('S')
+ ->setPercent((float)$taxRate)
+ ->setTaxScheme($taxScheme);
+
+ $taxableAmount = '0';
+ if (bccomp($taxRate, '0') > 0) {
+ $taxableAmount = bcdiv($taxAmount, bcdiv($taxRate, '100'), 10);
+ }
+
+ $taxSubTotal = (new TaxSubTotal())
+ ->setTaxableAmount((float)$taxableAmount)
+ ->setTaxAmount((float)$taxAmount)
+ ->setTaxCategory($taxCategory);
+
+ $taxSubTotals[] = $taxSubTotal;
+ $totalTax = bcadd($totalTax, $taxAmount);
+ }
+ }
+
+ $taxTotal = new TaxTotal();
+ $taxTotal->setTaxAmount((float)$totalTax);
+ foreach ($taxSubTotals as $subTotal) {
+ $taxTotal->addTaxSubTotal($subTotal);
+ }
+
+ return $taxTotal;
+ }
+
+ protected function buildMonetaryTotal(array $saleData): LegalMonetaryTotal
+ {
+ $subtotal = (string)($saleData['subtotal'] ?? 0);
+ $total = (string)($saleData['total'] ?? 0);
+ $amountDue = (string)($saleData['amount_due'] ?? 0);
+
+ return (new LegalMonetaryTotal())
+ ->setLineExtensionAmount((float)$subtotal)
+ ->setTaxExclusiveAmount((float)$subtotal)
+ ->setTaxInclusiveAmount((float)$total)
+ ->setPayableAmount((float)$amountDue);
+ }
+
+ protected function parseAddress(string $address): array
+ {
+ $parts = array_filter(array_map('trim', explode("\n", $address)));
+
+ $result = [
+ 'street' => '',
+ 'number' => '',
+ 'city' => '',
+ 'zip' => ''
+ ];
+
+ if (!empty($parts)) {
+ $result['street'] = $parts[0];
+ if (isset($parts[1])) {
+ // Match 4-5 digit postal codes (e.g., 1234, 12345) followed by city name
+ // Note: This handles common European formats. International formats
+ // like UK postcodes (e.g., "SW1A 2AA") may need additional handling.
+ if (preg_match('/(\d{4,5})\s*(.+)/', $parts[1], $matches)) {
+ $result['zip'] = $matches[1];
+ $result['city'] = $matches[2];
+ } else {
+ $result['city'] = $parts[1];
+ }
+ }
+ }
+
+ return $result;
+ }
+}
\ No newline at end of file
diff --git a/app/Views/sales/invoice.php b/app/Views/sales/invoice.php
index 654dab96f..3f83dcf86 100644
--- a/app/Views/sales/invoice.php
+++ b/app/Views/sales/invoice.php
@@ -69,6 +69,7 @@ if (isset($error_message)) {
= ' ' . lang('Sales.send_invoice') ?>
+ = anchor("sales/ublInvoice/$sale_id_num", ' ' . lang('Sales.download_ubl'), ['class' => 'btn btn-info btn-sm']) ?>
= anchor("sales", ' ' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_sales_button']) ?>
= anchor("sales/manage", ' ' . lang('Sales.takings'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_takings_button']) ?>
diff --git a/composer.json b/composer.json
index 296c5b921..881a52cc8 100644
--- a/composer.json
+++ b/composer.json
@@ -37,6 +37,7 @@
"dompdf/dompdf": "^2.0.3",
"ezyang/htmlpurifier": "^4.17",
"laminas/laminas-escaper": "2.17.0",
+ "num-num/ubl-invoice": "^2.4",
"paragonie/random_compat": "^2.0.21",
"picqer/php-barcode-generator": "^2.4.0",
"tamtamchik/namecase": "^3.0.0"
diff --git a/composer.lock b/composer.lock
index 9817fb506..68411a85a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,20 +4,89 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "3fe9e4622879914bfa763b71c236c7fe",
+ "content-hash": "294dd1f7f42c4cfd6a151cdb68c07a9f",
"packages": [
{
- "name": "codeigniter4/framework",
- "version": "v4.6.3",
+ "name": "carbonphp/carbon-doctrine-types",
+ "version": "3.2.0",
"source": {
"type": "git",
- "url": "https://github.com/codeigniter4/framework.git",
- "reference": "68d1a5896106f869452dd369a690dd5bc75160fb"
+ "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
+ "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/codeigniter4/framework/zipball/68d1a5896106f869452dd369a690dd5bc75160fb",
- "reference": "68d1a5896106f869452dd369a690dd5bc75160fb",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
+ "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.0.0 || >=5.0.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^4.0.0",
+ "nesbot/carbon": "^2.71.0 || ^3.0.0",
+ "phpunit/phpunit": "^10.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KyleKatarn",
+ "email": "kylekatarnls@gmail.com"
+ }
+ ],
+ "description": "Types to use Carbon in Doctrine",
+ "keywords": [
+ "carbon",
+ "date",
+ "datetime",
+ "doctrine",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
+ "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-02-09T16:56:22+00:00"
+ },
+ {
+ "name": "codeigniter4/framework",
+ "version": "v4.6.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/codeigniter4/framework.git",
+ "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/codeigniter4/framework/zipball/116e0919590a412c09d2b9e4f6b8addda18224d8",
+ "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8",
"shasum": ""
},
"require": {
@@ -31,7 +100,7 @@
"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",
@@ -78,7 +147,141 @@
"slack": "https://codeigniterchat.slack.com",
"source": "https://github.com/codeigniter4/CodeIgniter4"
},
- "time": "2025-08-02T13:36:13+00:00"
+ "time": "2026-02-01T17:59:34+00:00"
+ },
+ {
+ "name": "doctrine/collections",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/collections.git",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1",
+ "php": "^8.1",
+ "symfony/polyfill-php84": "^1.30"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ext-json": "*",
+ "phpstan/phpstan": "^2.1.30",
+ "phpstan/phpstan-phpunit": "^2.0.7",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Collections\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.",
+ "homepage": "https://www.doctrine-project.org/projects/collections.html",
+ "keywords": [
+ "array",
+ "collections",
+ "iterators",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/collections/issues",
+ "source": "https://github.com/doctrine/collections/tree/2.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-15T10:01:58+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=14"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
+ },
+ "time": "2026-02-07T07:09:04+00:00"
},
{
"name": "dompdf/dompdf",
@@ -331,6 +534,186 @@
},
"time": "2025-07-25T09:04:22+00:00"
},
+ {
+ "name": "nesbot/carbon",
+ "version": "3.11.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/CarbonPHP/carbon.git",
+ "reference": "6a7e652845bb018c668220c2a545aded8594fbbf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf",
+ "reference": "6a7e652845bb018c668220c2a545aded8594fbbf",
+ "shasum": ""
+ },
+ "require": {
+ "carbonphp/carbon-doctrine-types": "<100.0",
+ "ext-json": "*",
+ "php": "^8.1",
+ "psr/clock": "^1.0",
+ "symfony/clock": "^6.3.12 || ^7.0 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^3.6.3 || ^4.0",
+ "doctrine/orm": "^2.15.2 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^v3.87.1",
+ "kylekatarnls/multi-tester": "^2.5.3",
+ "phpmd/phpmd": "^2.15.0",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^2.1.22",
+ "phpunit/phpunit": "^10.5.53",
+ "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
+ },
+ "bin": [
+ "bin/carbon"
+ ],
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Carbon\\Laravel\\ServiceProvider"
+ ]
+ },
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-2.x": "2.x-dev",
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Carbon\\": "src/Carbon/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brian Nesbitt",
+ "email": "brian@nesbot.com",
+ "homepage": "https://markido.com"
+ },
+ {
+ "name": "kylekatarnls",
+ "homepage": "https://github.com/kylekatarnls"
+ }
+ ],
+ "description": "An API extension for DateTime that supports 281 different languages.",
+ "homepage": "https://carbonphp.github.io/carbon/",
+ "keywords": [
+ "date",
+ "datetime",
+ "time"
+ ],
+ "support": {
+ "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html",
+ "issues": "https://github.com/CarbonPHP/carbon/issues",
+ "source": "https://github.com/CarbonPHP/carbon"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon#sponsor",
+ "type": "opencollective"
+ },
+ {
+ "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-11T17:23:39+00:00"
+ },
+ {
+ "name": "num-num/ubl-invoice",
+ "version": "v2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/num-num/ubl-invoice.git",
+ "reference": "30fc1d9a1a8bd347630052addd6405942a1f0103"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/num-num/ubl-invoice/zipball/30fc1d9a1a8bd347630052addd6405942a1f0103",
+ "reference": "30fc1d9a1a8bd347630052addd6405942a1f0103",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/collections": "^1.8 || ^2.0",
+ "nesbot/carbon": "^2.72 || ^3.11",
+ "php": "^7.4 || ^8.0",
+ "sabre/xml": "^4.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^6.11",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.6",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "NumNum\\UBL\\": [
+ "src"
+ ],
+ "NumNum\\UBL\\Tests\\": [
+ "tests"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bert Devriese",
+ "email": "bert@numnum.be",
+ "homepage": "https://www.numnum.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "A modern object-oriented PHP library to create valid UBL and Peppol BIS 3.0 files",
+ "homepage": "https://github.com/num-num/ubl-invoice",
+ "keywords": [
+ "E-Invoice",
+ "digital invoice",
+ "efff",
+ "einvoice",
+ "electronic invoice",
+ "euinvoice",
+ "invoice",
+ "peppol",
+ "peppol bis",
+ "peppol invoice",
+ "peppolbis",
+ "ubl",
+ "ubl invoice",
+ "ublinvoice",
+ "xml",
+ "xml invoice"
+ ],
+ "support": {
+ "issues": "https://github.com/num-num/ubl-invoice/issues",
+ "source": "https://github.com/num-num/ubl-invoice/tree/v2.4.1"
+ },
+ "time": "2026-02-09T10:22:18+00:00"
+ },
{
"name": "paragonie/random_compat",
"version": "v2.0.21",
@@ -562,6 +945,54 @@
],
"time": "2024-09-18T08:09:33+00:00"
},
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
{
"name": "sabberworm/php-css-parser",
"version": "v8.9.0",
@@ -628,6 +1059,709 @@
},
"time": "2025-07-11T13:20:48+00:00"
},
+ {
+ "name": "sabre/uri",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/uri.git",
+ "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/uri/zipball/4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc",
+ "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.94",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "rector/rector": "^2.3"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Uri\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Functions for making sense out of URIs.",
+ "homepage": "http://sabre.io/uri/",
+ "keywords": [
+ "rfc3986",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/uri/issues",
+ "source": "https://github.com/fruux/sabre-uri"
+ },
+ "time": "2026-04-01T08:19:11+00:00"
+ },
+ {
+ "name": "sabre/xml",
+ "version": "4.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/xml.git",
+ "reference": "53db7bad0953949fb61037fbf9b13b421492395c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/xml/zipball/53db7bad0953949fb61037fbf9b13b421492395c",
+ "reference": "53db7bad0953949fb61037fbf9b13b421492395c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "lib-libxml": ">=2.6.20",
+ "php": "^7.4 || ^8.0",
+ "sabre/uri": ">=2.0,<4.0.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.94",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6",
+ "rector/rector": "^2.3"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Deserializer/functions.php",
+ "lib/Serializer/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Xml\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Markus Staab",
+ "email": "markus.staab@redaxo.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/xml is an XML library that you may not hate.",
+ "homepage": "https://sabre.io/xml/",
+ "keywords": [
+ "XMLReader",
+ "XMLWriter",
+ "dom",
+ "xml"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/xml/issues",
+ "source": "https://github.com/fruux/sabre-xml"
+ },
+ "time": "2026-04-02T11:40:41+00:00"
+ },
+ {
+ "name": "symfony/clock",
+ "version": "v7.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/clock": "^1.0",
+ "symfony/polyfill-php83": "^1.28"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/tree/v7.4.8"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-23T08:48:59+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php83\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-08T02:45:35+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php84",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-24T13:30:11+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v7.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "33600f8489485425bfcddd0d983391038d3422e7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7",
+ "reference": "33600f8489485425bfcddd0d983391038d3422e7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^2.5.3|^3.3"
+ },
+ "conflict": {
+ "nikic/php-parser": "<5.0",
+ "symfony/config": "<6.4",
+ "symfony/console": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/http-kernel": "<6.4",
+ "symfony/service-contracts": "<2.5",
+ "symfony/twig-bundle": "<6.4",
+ "symfony/yaml": "<6.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3|3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^5.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/http-client-contracts": "^2.5|^3.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to internationalize your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v7.4.8"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977",
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-15T13:41:35+00:00"
+ },
{
"name": "tamtamchik/namecase",
"version": "3.0.0",
@@ -760,28 +1894,28 @@
},
{
"name": "codeigniter/coding-standard",
- "version": "v1.8.8",
+ "version": "v1.9.1",
"source": {
"type": "git",
"url": "https://github.com/CodeIgniter/coding-standard.git",
- "reference": "410526fc1447a04fcdf5441b9c8507668b97b3a7"
+ "reference": "c7d227d3d3d0f2270405c8317c5e8c55f2262956"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CodeIgniter/coding-standard/zipball/410526fc1447a04fcdf5441b9c8507668b97b3a7",
- "reference": "410526fc1447a04fcdf5441b9c8507668b97b3a7",
+ "url": "https://api.github.com/repos/CodeIgniter/coding-standard/zipball/c7d227d3d3d0f2270405c8317c5e8c55f2262956",
+ "reference": "c7d227d3d3d0f2270405c8317c5e8c55f2262956",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "friendsofphp/php-cs-fixer": "^3.76",
- "nexusphp/cs-config": "^3.26",
- "php": "^8.1"
+ "friendsofphp/php-cs-fixer": "^3.94",
+ "nexusphp/cs-config": "^3.28",
+ "php": "^8.2"
},
"require-dev": {
- "nexusphp/tachycardia": "^2.3",
- "phpstan/phpstan": "^2.0",
- "phpunit/phpunit": "^10.5 || ^11.2"
+ "nexusphp/tachycardia": "^2.4",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^11.5 || ^12.5"
},
"type": "library",
"autoload": {
@@ -810,7 +1944,7 @@
"slack": "https://codeigniterchat.slack.com",
"source": "https://github.com/CodeIgniter/coding-standard"
},
- "time": "2025-09-27T13:54:11+00:00"
+ "time": "2026-02-17T18:41:24+00:00"
},
{
"name": "composer/pcre",
@@ -1207,16 +2341,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.90.0",
+ "version": "v3.94.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155"
+ "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ad732c2e9299c9743f9c55ae53cc0e7642ab1155",
- "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63",
+ "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63",
"shasum": ""
},
"require": {
@@ -1233,7 +2367,7 @@
"react/event-loop": "^1.5",
"react/socket": "^1.16",
"react/stream": "^1.4",
- "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
+ "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0",
"symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
@@ -1247,17 +2381,18 @@
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
},
"require-dev": {
- "facile-it/paraunit": "^1.3.1 || ^2.7",
- "infection/infection": "^0.31.0",
- "justinrainbow/json-schema": "^6.5",
- "keradus/cli-executor": "^2.2",
+ "facile-it/paraunit": "^1.3.1 || ^2.7.1",
+ "infection/infection": "^0.32.3",
+ "justinrainbow/json-schema": "^6.6.4",
+ "keradus/cli-executor": "^2.3",
"mikey179/vfsstream": "^1.6.12",
- "php-coveralls/php-coveralls": "^2.9",
- "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
- "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
- "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
- "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0",
- "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0"
+ "php-coveralls/php-coveralls": "^2.9.1",
+ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7",
+ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7",
+ "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51",
+ "symfony/polyfill-php85": "^1.33",
+ "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4",
+ "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
@@ -1272,7 +2407,7 @@
"PhpCsFixer\\": "src/"
},
"exclude-from-classmap": [
- "src/Fixer/Internal/*"
+ "src/**/Internal/"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -1298,7 +2433,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
- "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.90.0"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2"
},
"funding": [
{
@@ -1306,7 +2441,7 @@
"type": "github"
}
],
- "time": "2025-11-20T15:15:16+00:00"
+ "time": "2026-02-20T16:13:53+00:00"
},
{
"name": "kint-php/kint",
@@ -1487,33 +2622,30 @@
},
{
"name": "nexusphp/cs-config",
- "version": "v3.26.4",
+ "version": "v3.28.1",
"source": {
"type": "git",
"url": "https://github.com/NexusPHP/cs-config.git",
- "reference": "21cddae9917ec7b98e0b6890540222b658a4bf6e"
+ "reference": "4175b75a053b35dc64f37727df526520efb456f8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/NexusPHP/cs-config/zipball/21cddae9917ec7b98e0b6890540222b658a4bf6e",
- "reference": "21cddae9917ec7b98e0b6890540222b658a4bf6e",
+ "url": "https://api.github.com/repos/NexusPHP/cs-config/zipball/4175b75a053b35dc64f37727df526520efb456f8",
+ "reference": "4175b75a053b35dc64f37727df526520efb456f8",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "friendsofphp/php-cs-fixer": "^3.84",
- "php": "^8.1"
- },
- "conflict": {
- "liaison/cs-config": "*"
+ "friendsofphp/php-cs-fixer": "^3.94",
+ "php": "^8.2"
},
"require-dev": {
- "nexusphp/tachycardia": "^2.1",
+ "nexusphp/tachycardia": "^2.4",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
- "phpunit/phpunit": "^10.5 || ^11.0"
+ "phpunit/phpunit": "^11.5 || ^12.5"
},
"type": "library",
"autoload": {
@@ -1537,20 +2669,20 @@
"slack": "https://nexusphp.slack.com",
"source": "https://github.com/NexusPHP/cs-config.git"
},
- "time": "2025-09-27T13:51:19+00:00"
+ "time": "2026-02-22T12:03:01+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.6.2",
+ "version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "3a454ca033b9e06b63282ce19562e892747449bb"
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
- "reference": "3a454ca033b9e06b63282ce19562e892747449bb",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -1593,9 +2725,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
- "time": "2025-10-21T19:32:17+00:00"
+ "time": "2025-12-06T11:56:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -1717,35 +2849,35 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "11.0.11",
+ "version": "11.0.12",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
- "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56",
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^5.4.0",
+ "nikic/php-parser": "^5.7.0",
"php": ">=8.2",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1",
"sebastian/code-unit-reverse-lookup": "^4.0.1",
"sebastian/complexity": "^4.0.1",
- "sebastian/environment": "^7.2.0",
+ "sebastian/environment": "^7.2.1",
"sebastian/lines-of-code": "^3.0.1",
"sebastian/version": "^5.0.2",
- "theseer/tokenizer": "^1.2.3"
+ "theseer/tokenizer": "^1.3.1"
},
"require-dev": {
- "phpunit/phpunit": "^11.5.2"
+ "phpunit/phpunit": "^11.5.46"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -1783,7 +2915,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12"
},
"funding": [
{
@@ -1803,32 +2935,32 @@
"type": "tidelift"
}
],
- "time": "2025-08-27T14:37:49+00:00"
+ "time": "2025-12-24T07:01:01+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "5.1.0",
+ "version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
+ "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
- "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903",
+ "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "5.0-dev"
+ "dev-main": "5.1-dev"
}
},
"autoload": {
@@ -1856,15 +2988,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0"
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1"
},
"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/phpunit/php-file-iterator",
+ "type": "tidelift"
}
],
- "time": "2024-08-27T05:02:59+00:00"
+ "time": "2026-02-02T13:52:54+00:00"
},
{
"name": "phpunit/php-invoker",
@@ -2052,16 +3196,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.44",
+ "version": "11.5.55",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "c346885c95423eda3f65d85a194aaa24873cda82"
+ "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82",
- "reference": "c346885c95423eda3f65d85a194aaa24873cda82",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00",
+ "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00",
"shasum": ""
},
"require": {
@@ -2075,19 +3219,20 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
- "phpunit/php-code-coverage": "^11.0.11",
- "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-code-coverage": "^11.0.12",
+ "phpunit/php-file-iterator": "^5.1.1",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3",
- "sebastian/comparator": "^6.3.2",
+ "sebastian/comparator": "^6.3.3",
"sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.2",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
+ "sebastian/recursion-context": "^6.0.3",
"sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5"
@@ -2133,7 +3278,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55"
},
"funding": [
{
@@ -2157,7 +3302,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-13T07:17:35+00:00"
+ "time": "2026-02-18T12:37:06+00:00"
},
{
"name": "predis/predis",
@@ -2398,16 +3543,16 @@
},
{
"name": "react/child-process",
- "version": "v0.6.6",
+ "version": "v0.6.7",
"source": {
"type": "git",
"url": "https://github.com/reactphp/child-process.git",
- "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159"
+ "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159",
- "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159",
+ "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3",
+ "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3",
"shasum": ""
},
"require": {
@@ -2461,7 +3606,7 @@
],
"support": {
"issues": "https://github.com/reactphp/child-process/issues",
- "source": "https://github.com/reactphp/child-process/tree/v0.6.6"
+ "source": "https://github.com/reactphp/child-process/tree/v0.6.7"
},
"funding": [
{
@@ -2469,7 +3614,7 @@
"type": "open_collective"
}
],
- "time": "2025-01-01T16:37:48+00:00"
+ "time": "2025-12-23T15:25:20+00:00"
},
{
"name": "react/dns",
@@ -3022,16 +4167,16 @@
},
{
"name": "sebastian/comparator",
- "version": "6.3.2",
+ "version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
- "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
"shasum": ""
},
"require": {
@@ -3090,7 +4235,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3"
},
"funding": [
{
@@ -3110,7 +4255,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-10T08:07:46+00:00"
+ "time": "2026-01-24T09:26:40+00:00"
},
{
"name": "sebastian/complexity",
@@ -3890,16 +5035,16 @@
},
{
"name": "symfony/console",
- "version": "v7.3.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a"
+ "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
- "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
+ "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
+ "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
"shasum": ""
},
"require": {
@@ -3907,7 +5052,7 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^7.2"
+ "symfony/string": "^7.2|^8.0"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
@@ -3921,16 +5066,16 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/lock": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -3964,7 +5109,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.3.6"
+ "source": "https://github.com/symfony/console/tree/v7.4.8"
},
"funding": [
{
@@ -3984,87 +5129,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-04T01:21:42+00:00"
- },
- {
- "name": "symfony/deprecation-contracts",
- "version": "v3.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/contracts",
- "name": "symfony/contracts"
- },
- "branch-alias": {
- "dev-main": "3.6-dev"
- }
- },
- "autoload": {
- "files": [
- "function.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "A generic function and convention to trigger deprecation notices",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2024-09-25T14:21:43+00:00"
+ "time": "2026-03-30T13:54:39+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.3.3",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
+ "reference": "f57b899fa736fd71121168ef268f23c206083f0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191",
- "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a",
+ "reference": "f57b899fa736fd71121168ef268f23c206083f0a",
"shasum": ""
},
"require": {
@@ -4081,13 +5159,14 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/stopwatch": "^6.4|^7.0"
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4115,7 +5194,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8"
},
"funding": [
{
@@ -4135,7 +5214,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-13T11:49:31+00:00"
+ "time": "2026-03-30T13:54:39+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -4215,16 +5294,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.3.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a"
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a",
- "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5",
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5",
"shasum": ""
},
"require": {
@@ -4233,7 +5312,7 @@
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^6.4|^7.0"
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4261,7 +5340,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.3.6"
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.8"
},
"funding": [
{
@@ -4281,27 +5360,27 @@
"type": "tidelift"
}
],
- "time": "2025-11-05T09:52:27+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.3.5",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "9f696d2f1e340484b4683f7853b273abff94421f"
+ "reference": "e0be088d22278583a82da281886e8c3592fbf149"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f",
- "reference": "9f696d2f1e340484b4683f7853b273abff94421f",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149",
+ "reference": "e0be088d22278583a82da281886e8c3592fbf149",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "symfony/filesystem": "^6.4|^7.0"
+ "symfony/filesystem": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4329,7 +5408,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.3.5"
+ "source": "https://github.com/symfony/finder/tree/v7.4.8"
},
"funding": [
{
@@ -4349,20 +5428,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-15T18:45:57+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/options-resolver",
- "version": "v7.3.3",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d"
+ "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
- "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
+ "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
"shasum": ""
},
"require": {
@@ -4400,7 +5479,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v7.3.3"
+ "source": "https://github.com/symfony/options-resolver/tree/v7.4.8"
},
"funding": [
{
@@ -4420,7 +5499,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-05T10:16:07+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -4672,91 +5751,6 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
- {
- "name": "symfony/polyfill-mbstring",
- "version": "v1.33.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "shasum": ""
- },
- "require": {
- "ext-iconv": "*",
- "php": ">=7.2"
- },
- "provide": {
- "ext-mbstring": "*"
- },
- "suggest": {
- "ext-mbstring": "For best performance"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/polyfill",
- "name": "symfony/polyfill"
- }
- },
- "autoload": {
- "files": [
- "bootstrap.php"
- ],
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for the Mbstring extension",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "mbstring",
- "polyfill",
- "portable",
- "shim"
- ],
- "support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2024-12-23T08:48:59+00:00"
- },
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
@@ -4921,98 +5915,18 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
- {
- "name": "symfony/polyfill-php84",
- "version": "v1.33.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php84.git",
- "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
- "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/polyfill",
- "name": "symfony/polyfill"
- }
- },
- "autoload": {
- "files": [
- "bootstrap.php"
- ],
- "psr-4": {
- "Symfony\\Polyfill\\Php84\\": ""
- },
- "classmap": [
- "Resources/stubs"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "shim"
- ],
- "support": {
- "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-06-24T13:30:11+00:00"
- },
{
"name": "symfony/process",
- "version": "v7.3.4",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
- "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
+ "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a",
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a",
"shasum": ""
},
"require": {
@@ -5044,7 +5958,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.3.4"
+ "source": "https://github.com/symfony/process/tree/v7.4.8"
},
"funding": [
{
@@ -5064,7 +5978,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T10:12:26+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/service-contracts",
@@ -5155,16 +6069,16 @@
},
{
"name": "symfony/stopwatch",
- "version": "v7.3.0",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
- "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd"
+ "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd",
- "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89",
+ "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89",
"shasum": ""
},
"require": {
@@ -5197,7 +6111,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/stopwatch/tree/v7.3.0"
+ "source": "https://github.com/symfony/stopwatch/tree/v7.4.8"
},
"funding": [
{
@@ -5208,31 +6122,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-02-24T10:49:57+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/string",
- "version": "v7.3.4",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "f96476035142921000338bad71e5247fbc138872"
+ "reference": "114ac57257d75df748eda23dd003878080b8e688"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
- "reference": "f96476035142921000338bad71e5247fbc138872",
+ "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688",
+ "reference": "114ac57257d75df748eda23dd003878080b8e688",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-grapheme": "~1.33",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
},
@@ -5240,11 +6159,11 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/emoji": "^7.1",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
+ "symfony/emoji": "^7.1|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3.0",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -5283,7 +6202,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.3.4"
+ "source": "https://github.com/symfony/string/tree/v7.4.8"
},
"funding": [
{
@@ -5303,7 +6222,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T14:36:48+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "theseer/tokenizer",
diff --git a/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php b/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php
new file mode 100644
index 000000000..84fef6bf2
--- /dev/null
+++ b/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php
@@ -0,0 +1,121 @@
+assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
+ }
+
+ public function testCreateFromConfigUblOnly(): void
+ {
+ $generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
+ $this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
+ }
+
+ public function testCreateFromConfigBoth(): void
+ {
+ $generator = InvoiceAttachmentGenerator::createFromConfig('both');
+ $this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
+ }
+
+ public function testCreateFromConfigPdfOnlyRegistersPdfAttachment(): void
+ {
+ $generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only');
+ $attachments = $this->getPrivateProperty($generator, 'attachments');
+
+ $this->assertCount(1, $attachments);
+ $this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
+ }
+
+ public function testCreateFromConfigUblOnlyRegistersUblAttachment(): void
+ {
+ $generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
+ $attachments = $this->getPrivateProperty($generator, 'attachments');
+
+ $this->assertCount(1, $attachments);
+ $this->assertInstanceOf(UblAttachment::class, $attachments[0]);
+ }
+
+ public function testCreateFromConfigBothRegistersBothAttachments(): void
+ {
+ $generator = InvoiceAttachmentGenerator::createFromConfig('both');
+ $attachments = $this->getPrivateProperty($generator, 'attachments');
+
+ $this->assertCount(2, $attachments);
+ $this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
+ $this->assertInstanceOf(UblAttachment::class, $attachments[1]);
+ }
+
+ public function testRegisterAddsAttachment(): void
+ {
+ $generator = new InvoiceAttachmentGenerator();
+ $mockAttachment = new class implements InvoiceAttachment {
+ public function generate(array $saleData, string $type): ?string { return null; }
+ public function isApplicableForType(string $type, array $saleData): bool { return true; }
+ public function getFileExtension(): string { return 'test'; }
+ public function getEnabledConfigValues(): array { return ['test']; }
+ };
+
+ $result = $generator->register($mockAttachment);
+
+ $this->assertSame($generator, $result);
+ $attachments = $this->getPrivateProperty($generator, 'attachments');
+ $this->assertCount(1, $attachments);
+ }
+
+ public function testRegisterIsChainable(): void
+ {
+ $generator = new InvoiceAttachmentGenerator();
+ $mockAttachment = new class implements InvoiceAttachment {
+ public function generate(array $saleData, string $type): ?string { return null; }
+ public function isApplicableForType(string $type, array $saleData): bool { return true; }
+ public function getFileExtension(): string { return 'test'; }
+ public function getEnabledConfigValues(): array { return ['test']; }
+ };
+
+ $result = $generator->register($mockAttachment)->register($mockAttachment);
+
+ $attachments = $this->getPrivateProperty($result, 'attachments');
+ $this->assertCount(2, $attachments);
+ }
+
+ public function testGenerateAttachmentsReturnsEmptyArrayWhenNoAttachmentsRegistered(): void
+ {
+ $generator = new InvoiceAttachmentGenerator();
+ $result = $generator->generateAttachments([], 'invoice');
+
+ $this->assertIsArray($result);
+ $this->assertEmpty($result);
+ }
+
+ public function testCleanupRemovesFiles(): void
+ {
+ $tempFile1 = tempnam(sys_get_temp_dir(), 'test_');
+ $tempFile2 = tempnam(sys_get_temp_dir(), 'test_');
+
+ $this->assertFileExists($tempFile1);
+ $this->assertFileExists($tempFile2);
+
+ InvoiceAttachmentGenerator::cleanup([$tempFile1, $tempFile2]);
+
+ $this->assertFileDoesNotExist($tempFile1);
+ $this->assertFileDoesNotExist($tempFile2);
+ }
+
+ public function testCleanupHandlesNonExistentFiles(): void
+ {
+ // Should not throw an exception
+ InvoiceAttachmentGenerator::cleanup(['/non/existent/file1', '/non/existent/file2']);
+ $this->assertTrue(true);
+ }
+}
\ No newline at end of file
diff --git a/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php b/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
new file mode 100644
index 000000000..6c8b92d07
--- /dev/null
+++ b/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
@@ -0,0 +1,70 @@
+attachment = new PdfAttachment();
+ }
+
+ public function testGetFileExtensionReturnsPdf(): void
+ {
+ $this->assertEquals('pdf', $this->attachment->getFileExtension());
+ }
+
+ public function testGetEnabledConfigValuesReturnsCorrectArray(): void
+ {
+ $values = $this->attachment->getEnabledConfigValues();
+
+ $this->assertIsArray($values);
+ $this->assertContains('pdf_only', $values);
+ $this->assertContains('both', $values);
+ $this->assertCount(2, $values);
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForInvoice(): void
+ {
+ $this->assertTrue($this->attachment->isApplicableForType('invoice', []));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForTaxInvoice(): void
+ {
+ $this->assertTrue($this->attachment->isApplicableForType('tax_invoice', []));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForQuote(): void
+ {
+ $this->assertTrue($this->attachment->isApplicableForType('quote', []));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForWorkOrder(): void
+ {
+ $this->assertTrue($this->attachment->isApplicableForType('work_order', []));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForReceipt(): void
+ {
+ $this->assertTrue($this->attachment->isApplicableForType('receipt', []));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForAnyType(): void
+ {
+ // PDF should work for any document type
+ $this->assertTrue($this->attachment->isApplicableForType('random_type', []));
+ }
+
+ public function testIsApplicableForTypeIgnoresSaleData(): void
+ {
+ // PDF attachment doesn't depend on invoice_number
+ $this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => null]));
+ $this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => 'INV-001']));
+ }
+}
\ No newline at end of file
diff --git a/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php b/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
new file mode 100644
index 000000000..3546e32d4
--- /dev/null
+++ b/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
@@ -0,0 +1,103 @@
+attachment = new UblAttachment();
+ }
+
+ public function testGetFileExtensionReturnsXml(): void
+ {
+ $this->assertEquals('xml', $this->attachment->getFileExtension());
+ }
+
+ public function testGetEnabledConfigValuesReturnsCorrectArray(): void
+ {
+ $values = $this->attachment->getEnabledConfigValues();
+
+ $this->assertIsArray($values);
+ $this->assertContains('ubl_only', $values);
+ $this->assertContains('both', $values);
+ $this->assertCount(2, $values);
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForInvoiceWithInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertTrue($this->attachment->isApplicableForType('invoice', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsTrueForTaxInvoiceWithInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertTrue($this->attachment->isApplicableForType('tax_invoice', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => null];
+
+ $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForInvoiceWithEmptyInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => ''];
+
+ $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumberKey(): void
+ {
+ $saleData = [];
+
+ $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForQuoteEvenWithInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertFalse($this->attachment->isApplicableForType('quote', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForWorkOrderEvenWithInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertFalse($this->attachment->isApplicableForType('work_order', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForReceiptEvenWithInvoiceNumber(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertFalse($this->attachment->isApplicableForType('receipt', $saleData));
+ }
+
+ public function testIsApplicableForTypeReturnsFalseForUnknownType(): void
+ {
+ $saleData = ['invoice_number' => 'INV-001'];
+
+ $this->assertFalse($this->attachment->isApplicableForType('unknown_type', $saleData));
+ }
+
+ public function testGenerateReturnsNullForMissingConfig(): void
+ {
+ // Without proper sale_data, generate should fail gracefully
+ $result = $this->attachment->generate([], 'invoice');
+
+ $this->assertNull($result);
+ }
+}
\ No newline at end of file