RESAZIP-PC\resaz

update

...@@ -90,3 +90,62 @@ extension=gmp ...@@ -90,3 +90,62 @@ extension=gmp
90 extension=bcmath 90 extension=bcmath
91 # 3) Restart web server / PHP-FPM service 91 # 3) Restart web server / PHP-FPM service
92 ``` 92 ```
93 +
94 +## QueryLog (Laravel shared package)
95 +
96 +### 1) Publish config in child project
97 +```bash
98 +php artisan vendor:publish --tag=query-log-config
99 +```
100 +
101 +This creates `config/query_log.php` so each project can tune its own settings.
102 +
103 +### 2) Publish migration in child project
104 +```bash
105 +php artisan vendor:publish --tag=query-log-migrations
106 +php artisan migrate
107 +```
108 +
109 +This publishes a production-oriented migration for `log_queries` with key indexes:
110 +
111 +- `query_at`
112 +- `query_type`
113 +- `connection`
114 +- `user_id`
115 +- composite indexes for common filter windows
116 +
117 +### 3) Minimal table fields
118 +
119 +Use your own migration and ensure these columns exist in the configured table (`query_log.table`):
120 +
121 +- `action` (string)
122 +- `query` (longText/text)
123 +- `query_type` (string, example: `insert`, `update`, `delete`, `replace`)
124 +- `query_time` (float/double)
125 +- `query_at` (datetime)
126 +- `query_order` (int)
127 +- `connection` (string)
128 +- `ip` (nullable string)
129 +- `user_id` (nullable string/int)
130 +- `is_screen` (tinyint/bool)
131 +
132 +### 4) Important config for production
133 +
134 +- `enable`: enable/disable query log
135 +- `min_time`: skip very fast queries (ms)
136 +- `sample_rate`: sampling percent (`0-100`) to reduce high-traffic load
137 +- `chunk`: batch size per queue job
138 +- `max_queries_per_request`: hard limit per request
139 +- `skip_route_patterns`: wildcard route/url patterns to skip
140 +- `skip_command_patterns`: wildcard console command patterns to skip
141 +- `mask_sensitive_bindings`: mask sensitive values in SQL bindings
142 +- `sensitive_keywords`: keyword list used for masking
143 +- `masked_value`: replacement text for sensitive bindings
144 +
145 +### 5) Behavior highlights
146 +
147 +- Query buffer is separated by DB connection.
148 +- Buffer is flushed when transaction commits (outermost level).
149 +- Buffer is cleared on rollback (rolled-back queries are not logged).
150 +- Write-query detection supports CTE (`WITH ... UPDATE/INSERT/...`) and skips read queries.
151 +- `query_type` stores action text directly (`insert`, `update`, `delete`, `replace`, `upsert`).
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -10,10 +10,18 @@ ...@@ -10,10 +10,18 @@
10 "require": { 10 "require": {
11 "brick/math": "^0.9 || ^0.10 || ^0.11 || ^0.13", 11 "brick/math": "^0.9 || ^0.10 || ^0.11 || ^0.13",
12 "php": "^7.3 || ^8.0", 12 "php": "^7.3 || ^8.0",
13 - "laravel/framework": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" 13 + "laravel/framework": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
14 + "milon/barcode": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"
14 }, 15 },
15 "suggest": { 16 "suggest": {
16 "ext-gmp": "Faster big-number calculations", 17 "ext-gmp": "Faster big-number calculations",
17 "ext-bcmath": "Faster decimal calculations when GMP is unavailable" 18 "ext-bcmath": "Faster decimal calculations when GMP is unavailable"
19 + },
20 + "extra": {
21 + "laravel": {
22 + "providers": [
23 + "Daito\\Lib\\QueryLog\\Providers\\QueryLogProvider"
24 + ]
25 + }
18 } 26 }
19 } 27 }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 - "content-hash": "0c23b9705a5fe293b678d1bbc5020207", 7 + "content-hash": "94cb3df5cd945ab2de60f59263dbd8d6",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "brick/math", 10 "name": "brick/math",
...@@ -1144,6 +1144,81 @@ ...@@ -1144,6 +1144,81 @@
1144 "time": "2024-09-21T08:32:55+00:00" 1144 "time": "2024-09-21T08:32:55+00:00"
1145 }, 1145 },
1146 { 1146 {
1147 + "name": "milon/barcode",
1148 + "version": "v12.1.0",
1149 + "source": {
1150 + "type": "git",
1151 + "url": "https://github.com/milon/barcode.git",
1152 + "reference": "052e601665cfb99e119a630b6116fab0eb30183e"
1153 + },
1154 + "dist": {
1155 + "type": "zip",
1156 + "url": "https://api.github.com/repos/milon/barcode/zipball/052e601665cfb99e119a630b6116fab0eb30183e",
1157 + "reference": "052e601665cfb99e119a630b6116fab0eb30183e",
1158 + "shasum": ""
1159 + },
1160 + "require": {
1161 + "ext-gd": "*",
1162 + "illuminate/support": "^7.0|^8.0|^9.0|^10.0 | ^11.0 | ^12.0",
1163 + "php": "^7.3 | ^8.0"
1164 + },
1165 + "type": "library",
1166 + "extra": {
1167 + "laravel": {
1168 + "aliases": {
1169 + "DNS1D": "Milon\\Barcode\\Facades\\DNS1DFacade",
1170 + "DNS2D": "Milon\\Barcode\\Facades\\DNS2DFacade"
1171 + },
1172 + "providers": [
1173 + "Milon\\Barcode\\BarcodeServiceProvider"
1174 + ]
1175 + }
1176 + },
1177 + "autoload": {
1178 + "psr-0": {
1179 + "Milon\\Barcode": "src/"
1180 + }
1181 + },
1182 + "notification-url": "https://packagist.org/downloads/",
1183 + "license": [
1184 + "LGPL-3.0"
1185 + ],
1186 + "authors": [
1187 + {
1188 + "name": "Nuruzzaman Milon",
1189 + "email": "contact@milon.im"
1190 + }
1191 + ],
1192 + "description": "Barcode generator like Qr Code, PDF417, C39, C39+, C39E, C39E+, C93, S25, S25+, I25, I25+, C128, C128A, C128B, C128C, 2-Digits UPC-Based Extention, 5-Digits UPC-Based Extention, EAN 8, EAN 13, UPC-A, UPC-E, MSI (Variation of Plessey code)",
1193 + "keywords": [
1194 + "CODABAR",
1195 + "CODE 128",
1196 + "CODE 39",
1197 + "barcode",
1198 + "datamatrix",
1199 + "ean",
1200 + "laravel",
1201 + "pdf417",
1202 + "qr code",
1203 + "qrcode"
1204 + ],
1205 + "support": {
1206 + "issues": "https://github.com/milon/barcode/issues",
1207 + "source": "https://github.com/milon/barcode/tree/v12.1.0"
1208 + },
1209 + "funding": [
1210 + {
1211 + "url": "https://paypal.me/nuruzzamanmilon",
1212 + "type": "custom"
1213 + },
1214 + {
1215 + "url": "https://github.com/milon",
1216 + "type": "github"
1217 + }
1218 + ],
1219 + "time": "2026-02-07T00:42:12+00:00"
1220 + },
1221 + {
1147 "name": "monolog/monolog", 1222 "name": "monolog/monolog",
1148 "version": "2.11.0", 1223 "version": "2.11.0",
1149 "source": { 1224 "source": {
......
1 +<?php
2 +namespace Daito\Lib;
3 +
4 +use Milon\Barcode\DNS1D;
5 +use Milon\Barcode\DNS2D;
6 +
7 +class DaitoBarcode {
8 + public static function getJan13Number($jan12)
9 + {
10 + // Kiểm tra định dạng
11 + if (!preg_match('/^\d{12}$/', $jan12)) {
12 + return $jan12;
13 + }
14 +
15 + $sum = 0;
16 + for ($i = 0; $i < 12; $i++) {
17 + $digit = (int) $jan12[$i];
18 + $sum += $i % 2 === 0 ? $digit : $digit * 3;
19 + }
20 +
21 + $checkDigit = (10 - ($sum % 10)) % 10;
22 +
23 + return $jan12 . $checkDigit;
24 + }
25 + public static function isJanEAN13($jan)
26 + {
27 + if (!preg_match('/^\d{13}$/', $jan)) {
28 + return false;
29 + }
30 +
31 + $jan12 = substr($jan, 0, 12);
32 + $ean13Jan = self::getJan13Number($jan12);
33 + return $ean13Jan === $jan;
34 + }
35 +
36 + public static function getJan8Number($jan7)
37 + {
38 + if (!preg_match('/^\d{7}$/', $jan7)) {
39 + return $jan7;
40 + }
41 +
42 + $sum = 0;
43 + for ($i = 0; $i < 7; $i++) {
44 + $digit = (int) $jan7[$i];
45 + $sum += $i % 2 === 0 ? $digit * 3 : $digit;
46 + }
47 +
48 + $checkDigit = (10 - ($sum % 10)) % 10;
49 +
50 + return $jan7 . $checkDigit;
51 + }
52 +
53 + public static function isJanEAN8($jan)
54 + {
55 + if (!preg_match('/^\d{8}$/', $jan)) {
56 + return false;
57 + }
58 +
59 + $jan7 = substr($jan, 0, 7);
60 + $ean8Jan = self::getJan8Number($jan7);
61 +
62 + return $ean8Jan === $jan;
63 + }
64 +
65 + public static function getBarcodeTypeJan($jan)
66 + {
67 + if (self::isJanEAN8($jan)) {
68 + return 'EAN8';
69 + }
70 +
71 + if (self::isJanEAN13($jan)) {
72 + return 'EAN13';
73 + }
74 +
75 + return 'C128';
76 + }
77 +
78 + public static function generateBarcodeJan($jan)
79 + {
80 + $typeJan = self::getBarcodeTypeJan($jan);
81 +
82 + if ($typeJan === 'EAN13') {
83 + $jan = substr($jan, 0, 12);
84 + } elseif ($typeJan === 'EAN8') {
85 + $jan = substr($jan, 0, 7);
86 + }
87 +
88 + $dns1d = new DNS1D();
89 +
90 + return $dns1d->getBarcodePNG($jan, $typeJan);
91 + }
92 +
93 + public static function generateBarcodeC128($text)
94 + {
95 + $dns1d = new DNS1D();
96 +
97 + return $dns1d->getBarcodePNG($text, 'C128');
98 + }
99 +
100 + public static function generateBarcodeC39($text)
101 + {
102 + $dns1d = new DNS1D();
103 +
104 + return $dns1d->getBarcodePNG($text, 'C39');
105 + }
106 +
107 + public static function generateBarcodeEAN13($ean13)
108 + {
109 + $dns1d = new DNS1D();
110 +
111 + return $dns1d->getBarcodePNG($ean13, 'EAN13');
112 + }
113 +
114 + public static function generateBarcodeEAN8($ean8)
115 + {
116 + $dns1d = new DNS1D();
117 +
118 + return $dns1d->getBarcodePNG($ean8, 'EAN8');
119 + }
120 +
121 + public static function generateBarcodeQrCode($text)
122 + {
123 + $dns2d = new DNS2D();
124 +
125 + return $dns2d->getBarcodePNG($text, 'QRCODE');
126 + }
127 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +namespace Daito\Lib;
4 +
5 +use Brick\Math\BigDecimal;
6 +use Brick\Math\RoundingMode;
7 +use RuntimeException;
8 +
9 +class DaitoNumber
10 +{
11 + const DEFAULT_MAX_DECIMALS = 12;
12 +
13 + /**
14 + * Format number with configurable separators, prefix/suffix, and decimal behavior.
15 + *
16 + * Example:
17 + * DaitoNumber::format('1234567.00') => '1,234,567'
18 + * DaitoNumber::format('1234567.5', array('decimals' => 2)) => '1,234,567.50'
19 + */
20 + public static function format($number, array $arrOptions = array())
21 + {
22 + $arrConfig = array_merge(
23 + array(
24 + 'thousands_separator' => ',',
25 + 'decimal_separator' => '.',
26 + 'prefix' => '',
27 + 'suffix' => '',
28 + 'decimals' => null,
29 + 'trim_trailing_zeros' => true,
30 + 'max_decimals' => self::DEFAULT_MAX_DECIMALS,
31 + ),
32 + $arrOptions
33 + );
34 +
35 + $numberString = self::toCanonicalString($number);
36 + $isNegative = strpos($numberString, '-') === 0;
37 + $unsigned = $isNegative ? substr($numberString, 1) : $numberString;
38 +
39 + if ($arrConfig['decimals'] !== null) {
40 + $decimals = (int) $arrConfig['decimals'];
41 + if ($decimals < 0) {
42 + throw new RuntimeException('decimals must be greater than or equal to 0.');
43 + }
44 +
45 + $unsigned = (string) BigDecimal::of($unsigned)->toScale($decimals, RoundingMode::HALF_UP);
46 + } else {
47 + $maxDecimals = (int) $arrConfig['max_decimals'];
48 + if ($maxDecimals < 0) {
49 + throw new RuntimeException('max_decimals must be greater than or equal to 0.');
50 + }
51 +
52 + $unsigned = (string) BigDecimal::of($unsigned)->toScale($maxDecimals, RoundingMode::HALF_UP);
53 + if ($arrConfig['trim_trailing_zeros']) {
54 + $unsigned = self::trimTrailingZeros($unsigned);
55 + }
56 + }
57 +
58 + $arrParts = explode('.', $unsigned, 2);
59 + $integerPart = self::addThousandsSeparator($arrParts[0], (string) $arrConfig['thousands_separator']);
60 + $decimalPart = isset($arrParts[1]) ? $arrParts[1] : '';
61 +
62 + $formattedNumber = $integerPart;
63 + if ($decimalPart !== '') {
64 + $formattedNumber .= (string) $arrConfig['decimal_separator'] . $decimalPart;
65 + }
66 +
67 + $formatted = (string) $arrConfig['prefix'] . $formattedNumber . (string) $arrConfig['suffix'];
68 +
69 + return $isNegative ? '-' . $formatted : $formatted;
70 + }
71 +
72 + /**
73 + * Format currency-friendly output (default 2 decimals, keep trailing zeros).
74 + *
75 + * Example:
76 + * DaitoNumber::formatCurrency('1234.5') => '1,234.50'
77 + * DaitoNumber::formatCurrency('1234.5', array('prefix' => '$')) => '$1,234.50'
78 + */
79 + public static function formatCurrency($number, array $arrOptions = array())
80 + {
81 + $arrCurrencyOptions = array_merge(
82 + array(
83 + 'decimals' => 2,
84 + 'trim_trailing_zeros' => false,
85 + 'prefix' => '',
86 + 'suffix' => '',
87 + ),
88 + $arrOptions
89 + );
90 +
91 + return self::format($number, $arrCurrencyOptions);
92 + }
93 +
94 + /**
95 + * Format percentage value.
96 + *
97 + * Example:
98 + * DaitoNumber::formatPercent('12.3456') => '12.35%'
99 + * DaitoNumber::formatPercent('0.1234', array('input_ratio' => true)) => '12.34%'
100 + */
101 + public static function formatPercent($number, array $arrOptions = array())
102 + {
103 + $arrPercentOptions = array_merge(
104 + array(
105 + 'decimals' => 2,
106 + 'trim_trailing_zeros' => true,
107 + 'suffix' => '%',
108 + 'input_ratio' => false,
109 + ),
110 + $arrOptions
111 + );
112 +
113 + $numberValue = $number;
114 + if ($arrPercentOptions['input_ratio']) {
115 + $numberValue = (string) BigDecimal::of(self::toCanonicalString($number))->multipliedBy('100');
116 + }
117 +
118 + unset($arrPercentOptions['input_ratio']);
119 +
120 + return self::format($numberValue, $arrPercentOptions);
121 + }
122 +
123 + private static function toCanonicalString($number)
124 + {
125 + if (is_int($number) || is_string($number)) {
126 + $numberString = trim((string) $number);
127 + } elseif (is_float($number)) {
128 + $numberString = self::trimTrailingZeros(sprintf('%.14F', $number));
129 + } else {
130 + throw new RuntimeException('DaitoNumber only supports int, float, or numeric string.');
131 + }
132 +
133 + if ($numberString === '' || !preg_match('/^[+-]?\d+(\.\d+)?$/', $numberString)) {
134 + throw new RuntimeException('Invalid number format.');
135 + }
136 +
137 + return (string) BigDecimal::of($numberString);
138 + }
139 +
140 + private static function addThousandsSeparator($integerPart, $thousandsSeparator)
141 + {
142 + return preg_replace('/\B(?=(\d{3})+(?!\d))/', $thousandsSeparator, $integerPart);
143 + }
144 +
145 + private static function trimTrailingZeros($numberString)
146 + {
147 + if (strpos($numberString, '.') === false) {
148 + return $numberString;
149 + }
150 +
151 + $numberString = rtrim($numberString, '0');
152 + $numberString = rtrim($numberString, '.');
153 +
154 + return $numberString === '' ? '0' : $numberString;
155 + }
156 +}
1 <?php 1 <?php
2 +
2 namespace Daito\Lib; 3 namespace Daito\Lib;
3 4
4 -class DaitoString { 5 +use RuntimeException;
5 - public static function toUpper($text){ 6 +
6 - return strtoupper($text); 7 +class DaitoString
8 +{
9 + const UTF8 = 'UTF-8';
10 + const NKF_SOURCE_ENCODINGS = 'SJIS-win,CP932,Shift_JIS,EUC-JP,UTF-8';
11 +
12 + /**
13 + * Collapse multiple spaces/tabs/newlines into a single ASCII space.
14 + *
15 + * Example:
16 + * DaitoString::collapseSpaces("a b\t\tc") => "a b c"
17 + */
18 + public static function collapseSpaces($input)
19 + {
20 + return preg_replace('/\s+/u', ' ', (string) $input);
21 + }
22 +
23 + /**
24 + * Convert full-width Japanese spaces to ASCII spaces.
25 + *
26 + * Example:
27 + * DaitoString::normalizeFullWidthSpace("A B") => "A B"
28 + */
29 + public static function normalizeFullWidthSpace($input)
30 + {
31 + return str_replace(' ', ' ', (string) $input);
32 + }
33 +
34 + /**
35 + * Convert half-width kana to full-width kana.
36 + *
37 + * Example:
38 + * DaitoString::toFullWidthKana("カタカナ") => "カタカナ"
39 + */
40 + public static function toFullWidthKana($text)
41 + {
42 + return mb_convert_kana((string) $text, 'KV', self::UTF8);
43 + }
44 +
45 + /**
46 + * Convert full-width kana to half-width kana.
47 + *
48 + * Example:
49 + * DaitoString::toHalfWidthKana("カタカナ") => "カタカナ"
50 + */
51 + public static function toHalfWidthKana($text)
52 + {
53 + return mb_convert_kana((string) $text, 'kV', self::UTF8);
54 + }
55 +
56 + /**
57 + * Split text by normalized spaces (full-width/extra spaces are handled).
58 + *
59 + * Example:
60 + * DaitoString::splitBySpace("A  B C") => array("A", "B", "C")
61 + */
62 + public static function splitBySpace($string)
63 + {
64 + $normalized = self::normalizeFullWidthSpace((string) $string);
65 + $normalized = trim(self::collapseSpaces($normalized));
66 +
67 + if ($normalized === '') {
68 + return array();
69 + }
70 +
71 + return explode(' ', $normalized);
72 + }
73 +
74 + /**
75 + * Convert Hiragana to Katakana.
76 + *
77 + * Example:
78 + * DaitoString::toKatakana("ひらがな") => "ヒラガナ"
79 + */
80 + public static function toKatakana($input)
81 + {
82 + return mb_convert_kana((string) $input, 'C', self::UTF8);
83 + }
84 +
85 + /**
86 + * Check whether the input contains Japanese characters.
87 + *
88 + * Example:
89 + * DaitoString::isJapanese("abc日本語") => true
90 + * DaitoString::isJapanese("abcdef") => false
91 + */
92 + public static function isJapanese($input)
93 + {
94 + return preg_match('/[\x{3040}-\x{30FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{FF66}-\x{FF9D}]/u', (string) $input) === 1;
95 + }
96 +
97 + /**
98 + * Convert Japanese-encoded text file to UTF-8.
99 + * Prefer nkf when available, fallback to mb_convert_encoding.
100 + *
101 + * Example:
102 + * DaitoString::convertToUtf8('/tmp/source.csv');
103 + * DaitoString::convertToUtf8('/tmp/source.csv', '/tmp/out', 'source_utf8.csv', 1);
104 + */
105 + public static function convertToUtf8($sourceFile, $destDir = '', $fileName = '', $isBk = 0)
106 + {
107 + $sourceFilePath = (string) $sourceFile;
108 + if (!is_file($sourceFilePath)) {
109 + throw new RuntimeException('Source file does not exist: ' . $sourceFilePath);
110 + }
111 +
112 + $targetDirectory = $destDir ? (string) $destDir : dirname($sourceFilePath);
113 + if (!is_dir($targetDirectory)) {
114 + throw new RuntimeException('Destination directory does not exist: ' . $targetDirectory);
115 + }
116 +
117 + $targetFileName = $fileName ? (string) $fileName : basename($sourceFilePath);
118 + $destFile = $targetDirectory . DIRECTORY_SEPARATOR . $targetFileName;
119 +
120 + if ((string) realpath($sourceFilePath) === (string) realpath($destFile)) {
121 + $destFile .= '_' . time();
122 + }
123 +
124 + if ($isBk) {
125 + $backupFile = $targetDirectory . DIRECTORY_SEPARATOR . $targetFileName . '.bak';
126 + if (!copy($sourceFilePath, $backupFile)) {
127 + throw new RuntimeException('Can not create backup file: ' . $backupFile);
7 } 128 }
8 - public static function toLower($text){ 129 + }
9 - return strtolower($text); 130 +
131 + $utf8Content = self::convertJapaneseTextToUtf8($sourceFilePath);
132 + if (file_put_contents($destFile, $utf8Content) === false) {
133 + throw new RuntimeException('Can not write destination file: ' . $destFile);
134 + }
135 +
136 + return $destFile;
137 + }
138 +
139 + private static function convertJapaneseTextToUtf8($sourceFilePath)
140 + {
141 + if (self::canUseNkf()) {
142 + $nkfContent = self::convertFileByNkf($sourceFilePath);
143 + if ($nkfContent !== null) {
144 + return $nkfContent;
145 + }
146 + }
147 +
148 + return self::convertFileToUtf8ByMbstring($sourceFilePath);
149 + }
150 +
151 + private static function canUseNkf()
152 + {
153 + if (!function_exists('shell_exec')) {
154 + return false;
155 + }
156 +
157 + $disabledFunctions = (string) ini_get('disable_functions');
158 + if ($disabledFunctions !== '' && strpos($disabledFunctions, 'shell_exec') !== false) {
159 + return false;
160 + }
161 +
162 + $output = @shell_exec('nkf --version');
163 +
164 + return is_string($output) && $output !== '';
165 + }
166 +
167 + private static function convertFileByNkf($sourceFilePath)
168 + {
169 + $command = 'nkf -w -- ' . escapeshellarg($sourceFilePath);
170 + $output = @shell_exec($command);
171 +
172 + return is_string($output) ? $output : null;
173 + }
174 +
175 + /**
176 + * Convert a file content to UTF-8 using mbstring.
177 + *
178 + * Example:
179 + * DaitoString::convertFileToUtf8ByMbstring('/tmp/source_sjis.txt');
180 + */
181 + public static function convertFileToUtf8ByMbstring($sourceFilePath)
182 + {
183 + $content = file_get_contents($sourceFilePath);
184 + if ($content === false) {
185 + throw new RuntimeException('Can not read source file: ' . $sourceFilePath);
186 + }
187 +
188 + return self::convertTextToUtf8ByMbstring($content);
189 + }
190 +
191 + /**
192 + * Convert raw text to UTF-8 using mbstring.
193 + *
194 + * Example:
195 + * DaitoString::convertTextToUtf8ByMbstring($rawText);
196 + */
197 + public static function convertTextToUtf8ByMbstring($text)
198 + {
199 + $utf8Content = mb_convert_encoding((string) $text, self::UTF8, self::NKF_SOURCE_ENCODINGS);
200 + if ($utf8Content === false) {
201 + throw new RuntimeException('Can not convert text to UTF-8.');
202 + }
203 +
204 + return $utf8Content;
10 } 205 }
11 } 206 }
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php
2 +
3 +namespace Daito\Lib\QueryLog\Jobs;
4 +
5 +use Illuminate\Contracts\Queue\ShouldQueue;
6 +use Illuminate\Bus\Queueable;
7 +use Illuminate\Foundation\Bus\Dispatchable;
8 +use Illuminate\Queue\InteractsWithQueue;
9 +use Illuminate\Queue\SerializesModels;
10 +use Illuminate\Support\Facades\DB;
11 +
12 +class SaveQueryLogJob implements ShouldQueue
13 +{
14 + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
15 +
16 + protected $arrQueries;
17 +
18 + public function __construct(array $arrQueries)
19 + {
20 + $this->arrQueries = $arrQueries;
21 + }
22 +
23 + public function handle(): void
24 + {
25 + if ($this->arrQueries === array()) {
26 + return;
27 + }
28 +
29 + DB::connection(config('query_log.connection', 'query_log'))
30 + ->table(config('query_log.table', 'log_queries'))
31 + ->insert($this->arrQueries);
32 + }
33 +}
1 +<?php
2 +
3 +namespace Daito\Lib\QueryLog\Models;
4 +
5 +use Illuminate\Database\Eloquent\Model;
6 +
7 +class QueryLog extends Model
8 +{
9 + /**
10 + * The table associated with the model.
11 + *
12 + * @var string
13 + */
14 + protected $table = 'log_queries';
15 +
16 + /**
17 + * The primary key for the model.
18 + *
19 + * @var string
20 + */
21 + protected $primaryKey = 'id';
22 +
23 + /**
24 + * Indicates if the IDs are auto-incrementing.
25 + *
26 + * @var bool
27 + */
28 + public $incrementing = true;
29 +
30 + /**
31 + * Indicates if the IDs are auto-incrementing.
32 + *
33 + * @var bool
34 + */
35 + public $timestamps = false;
36 +
37 + /**
38 + * The attributes that are mass assignable.
39 + *
40 + * @var array<int, string>
41 + */
42 + protected $fillable = [
43 + 'action',
44 + 'query',
45 + 'query_type',
46 + 'query_time',
47 + 'query_at',
48 + 'query_order',
49 + 'connection',
50 + 'ip',
51 + 'user_id',
52 + 'is_screen',
53 + ];
54 + protected $connection = 'query_log';
55 +
56 + public function __construct(array $arrAttributes = array())
57 + {
58 + parent::__construct($arrAttributes);
59 +
60 + $this->setConnection(config('query_log.connection', 'query_log'));
61 + $this->setTable(config('query_log.table', 'log_queries'));
62 + }
63 +}
1 +<?php
2 +
3 +namespace Daito\Lib\QueryLog\Providers;
4 +
5 +use Carbon\Carbon;
6 +use Daito\Lib\QueryLog\Jobs\SaveQueryLogJob;
7 +use Illuminate\Database\Connection;
8 +use Illuminate\Database\Events\QueryExecuted;
9 +use Illuminate\Database\Events\TransactionCommitted;
10 +use Illuminate\Database\Events\TransactionRolledBack;
11 +use Illuminate\Support\Facades\Auth;
12 +use Illuminate\Support\Facades\DB;
13 +use Illuminate\Support\Facades\Event;
14 +use Illuminate\Support\ServiceProvider;
15 +use Throwable;
16 +
17 +class QueryLogProvider extends ServiceProvider
18 +{
19 + private $arrQueriesByConnection = array();
20 + private $arrLoggedCountsByConnection = array();
21 +
22 + public function register(): void
23 + {
24 + $this->mergeConfigFrom(
25 + __DIR__ . '/../config/query_log.php',
26 + 'query_log'
27 + );
28 + }
29 +
30 + public function boot(): void
31 + {
32 + $this->registerPublishableResources();
33 + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
34 +
35 + if (!$this->isEnabled()) {
36 + return;
37 + }
38 +
39 + $this->registerTransactionLifecycleListeners();
40 +
41 + DB::listen(function (QueryExecuted $query) {
42 + if (!$this->isEnabled()) {
43 + return;
44 + }
45 +
46 + if (!$this->shouldLogInCurrentRuntime()) {
47 + return;
48 + }
49 +
50 + if ((float) $query->time < (float) config('query_log.min_time', 0)) {
51 + return;
52 + }
53 +
54 + if (!$this->isPassedSampling()) {
55 + return;
56 + }
57 +
58 + if ($this->shouldSkipCurrentContext()) {
59 + return;
60 + }
61 +
62 + $queryVerb = $this->detectWriteVerb($query->sql);
63 + if ($queryVerb === null) {
64 + return;
65 + }
66 +
67 + if ($this->isIgnoredTableSql($query->sql)) {
68 + return;
69 + }
70 +
71 + $connectionName = $query->connectionName ?: $query->connection->getName();
72 + if (!$this->canLogMoreQueries($connectionName)) {
73 + return;
74 + }
75 +
76 + $arrPayload = $this->buildPayload($query, $queryVerb, $connectionName);
77 + $this->appendQuery($connectionName, $arrPayload);
78 +
79 + if (!$this->isInTransaction($query->connection)) {
80 + $this->flushConnectionBuffer($connectionName);
81 + }
82 + });
83 +
84 + if (method_exists($this->app, 'terminating')) {
85 + call_user_func(array($this->app, 'terminating'), function () {
86 + $this->flushAllBuffers();
87 + });
88 + }
89 + }
90 +
91 + private function registerTransactionLifecycleListeners(): void
92 + {
93 + Event::listen(TransactionCommitted::class, function (TransactionCommitted $event) {
94 + if ($event->connection->transactionLevel() !== 0) {
95 + return;
96 + }
97 +
98 + $this->flushConnectionBuffer($event->connection->getName(), true);
99 + });
100 +
101 + Event::listen(TransactionRolledBack::class, function (TransactionRolledBack $event) {
102 + if ($event->connection->transactionLevel() !== 0) {
103 + return;
104 + }
105 +
106 + $this->clearConnectionBuffer($event->connection->getName());
107 + });
108 + }
109 +
110 + private function appendQuery($connectionName, array $arrPayload): void
111 + {
112 + if (!isset($this->arrQueriesByConnection[$connectionName])) {
113 + $this->arrQueriesByConnection[$connectionName] = array();
114 + }
115 +
116 + $this->arrQueriesByConnection[$connectionName][] = $arrPayload;
117 + $this->arrLoggedCountsByConnection[$connectionName] = ($this->arrLoggedCountsByConnection[$connectionName] ?? 0) + 1;
118 + }
119 +
120 + private function flushConnectionBuffer($connectionName, $force = false): void
121 + {
122 + $arrBuffer = $this->arrQueriesByConnection[$connectionName] ?? array();
123 + if ($arrBuffer === array()) {
124 + return;
125 + }
126 +
127 + $chunkSize = max(1, (int) config('query_log.chunk', 200));
128 + if (!$force && count($arrBuffer) < $chunkSize) {
129 + return;
130 + }
131 +
132 + foreach (array_chunk($arrBuffer, $chunkSize) as $arrQueries) {
133 + $job = new SaveQueryLogJob($arrQueries);
134 + if (config('query_log.queue_connection') !== null) {
135 + $job->onConnection(config('query_log.queue_connection'));
136 + }
137 + if (config('query_log.queue_name') !== null) {
138 + $job->onQueue(config('query_log.queue_name'));
139 + }
140 +
141 + dispatch($job);
142 + }
143 +
144 + $this->arrQueriesByConnection[$connectionName] = array();
145 + }
146 +
147 + private function flushAllBuffers(): void
148 + {
149 + foreach (array_keys($this->arrQueriesByConnection) as $connectionName) {
150 + $this->flushConnectionBuffer($connectionName, true);
151 + }
152 + }
153 +
154 + private function clearConnectionBuffer($connectionName): void
155 + {
156 + $this->arrQueriesByConnection[$connectionName] = array();
157 + }
158 +
159 + private function canLogMoreQueries($connectionName): bool
160 + {
161 + $maxQueries = max(1, (int) config('query_log.max_queries_per_request', 1000));
162 + $currentCount = $this->arrLoggedCountsByConnection[$connectionName] ?? 0;
163 +
164 + return $currentCount < $maxQueries;
165 + }
166 +
167 + private function isIgnoredTableSql($sql): bool
168 + {
169 + $arrIgnoreTables = (array) config('query_log.ignore_tables', array());
170 + if ($arrIgnoreTables === array()) {
171 + return false;
172 + }
173 +
174 + foreach ($arrIgnoreTables as $tableName) {
175 + $tablePattern = '/\b`?' . preg_quote((string) $tableName, '/') . '`?\b/i';
176 + if (preg_match($tablePattern, $sql) === 1) {
177 + return true;
178 + }
179 + }
180 +
181 + return false;
182 + }
183 +
184 + private function detectWriteVerb($sql)
185 + {
186 + $normalizedSql = trim($this->stripLeadingSqlComments((string) $sql));
187 + if ($normalizedSql === '') {
188 + return null;
189 + }
190 +
191 + if (preg_match('/^(insert|update|delete|replace|upsert)\b/i', $normalizedSql, $arrMatches) === 1) {
192 + return strtolower($arrMatches[1]);
193 + }
194 +
195 + if (preg_match('/^with\b/i', $normalizedSql) === 1
196 + && preg_match('/\b(insert|update|delete|replace|upsert)\b/i', $normalizedSql, $arrMatches) === 1
197 + ) {
198 + return strtolower($arrMatches[1]);
199 + }
200 +
201 + return null;
202 + }
203 +
204 + private function stripLeadingSqlComments($sql)
205 + {
206 + $cleanSql = preg_replace('/^\s*(\/\*.*?\*\/\s*)+/s', '', $sql);
207 + if ($cleanSql === null) {
208 + return $sql;
209 + }
210 +
211 + return preg_replace('/^\s*(--[^\r\n]*[\r\n]\s*)+/s', '', $cleanSql) ?: $cleanSql;
212 + }
213 +
214 + private function buildPayload(QueryExecuted $query, $queryVerb, $connectionName): array
215 + {
216 + $rawSql = $this->interpolateSql($query->sql, $query->bindings);
217 + $maxSqlLength = max(256, (int) config('query_log.max_sql_length', 4000));
218 + $sql = mb_substr($rawSql, 0, $maxSqlLength);
219 +
220 + return array(
221 + 'query' => $sql,
222 + 'action' => $this->resolveAction(),
223 + 'query_type' => $queryVerb,
224 + 'query_time' => $query->time,
225 + 'query_at' => Carbon::now(),
226 + 'query_order' => ($this->arrLoggedCountsByConnection[$connectionName] ?? 0) + 1,
227 + 'connection' => $connectionName,
228 + 'is_screen' => app()->runningInConsole() ? 0 : 1,
229 + 'user_id' => $this->resolveUserId(),
230 + 'ip' => request() ? request()->ip() : null,
231 + );
232 + }
233 +
234 + private function interpolateSql($sql, array $arrBindings): string
235 + {
236 + $interpolatedSql = (string) $sql;
237 + foreach ($arrBindings as $index => $binding) {
238 + $isSensitiveBinding = $this->isSensitiveBinding($interpolatedSql, (int) $index);
239 + $value = $this->quoteBinding($binding, $isSensitiveBinding);
240 + $interpolatedSql = preg_replace('/\?/', $value, $interpolatedSql, 1) ?: $interpolatedSql;
241 + }
242 +
243 + return str_replace(array("\r\n", "\n"), ' ', trim($interpolatedSql));
244 + }
245 +
246 + private function quoteBinding($binding, $isSensitive = false): string
247 + {
248 + if ($isSensitive && (bool) config('query_log.mask_sensitive_bindings', true)) {
249 + return "'" . (string) config('query_log.masked_value', '***') . "'";
250 + }
251 +
252 + if ($binding === null) {
253 + return 'null';
254 + }
255 + if (is_bool($binding)) {
256 + return $binding ? '1' : '0';
257 + }
258 + if (is_numeric($binding)) {
259 + return (string) $binding;
260 + }
261 + if ($binding instanceof \DateTimeInterface) {
262 + return "'" . $binding->format('Y-m-d H:i:s') . "'";
263 + }
264 +
265 + return "'" . str_replace("'", "''", (string) $binding) . "'";
266 + }
267 +
268 + private function isSensitiveBinding($interpolatedSql, int $bindingIndex): bool
269 + {
270 + $arrSensitiveKeywords = (array) config('query_log.sensitive_keywords', array());
271 + if ($arrSensitiveKeywords === array()) {
272 + return false;
273 + }
274 +
275 + $parts = explode('?', (string) $interpolatedSql);
276 + if (!isset($parts[$bindingIndex])) {
277 + return false;
278 + }
279 +
280 + $context = strtolower(substr($parts[$bindingIndex], -120));
281 + foreach ($arrSensitiveKeywords as $keyword) {
282 + $keywordText = strtolower((string) $keyword);
283 + if ($keywordText !== '' && strpos($context, $keywordText) !== false) {
284 + return true;
285 + }
286 + }
287 +
288 + return false;
289 + }
290 +
291 + private function resolveAction(): string
292 + {
293 + if (app()->runningInConsole()) {
294 + return $this->resolveConsoleCommand();
295 + }
296 +
297 + $request = request();
298 + if ($request === null) {
299 + return 'http';
300 + }
301 +
302 + return $request->method() . ' ' . $request->fullUrl();
303 + }
304 +
305 + private function resolveUserId()
306 + {
307 + try {
308 + return Auth::check() ? Auth::id() : null;
309 + } catch (Throwable $throwable) {
310 + return null;
311 + }
312 + }
313 +
314 + private function shouldSkipCurrentContext(): bool
315 + {
316 + if (app()->runningInConsole()) {
317 + $command = $this->resolveConsoleCommand();
318 + return $this->matchPatterns($command, (array) config('query_log.skip_command_patterns', array()));
319 + }
320 +
321 + $request = request();
322 + if ($request === null) {
323 + return false;
324 + }
325 +
326 + $route = $request->route();
327 + $routeName = is_object($route) && method_exists($route, 'getName') ? (string) $route->getName() : '';
328 + $routePath = ltrim((string) $request->path(), '/');
329 + $fullUrl = (string) $request->fullUrl();
330 +
331 + $arrTargets = array($routeName, $routePath, $fullUrl);
332 + foreach ($arrTargets as $target) {
333 + if ($this->matchPatterns($target, (array) config('query_log.skip_route_patterns', array()))) {
334 + return true;
335 + }
336 + }
337 +
338 + return false;
339 + }
340 +
341 + private function resolveConsoleCommand(): string
342 + {
343 + $arrArgv = $_SERVER['argv'] ?? array();
344 + return isset($arrArgv[1]) ? trim((string) $arrArgv[1]) : 'console';
345 + }
346 +
347 + private function matchPatterns(string $target, array $arrPatterns): bool
348 + {
349 + if ($target === '' || $arrPatterns === array()) {
350 + return false;
351 + }
352 +
353 + foreach ($arrPatterns as $pattern) {
354 + $patternText = (string) $pattern;
355 + if ($patternText !== '' && $this->matchPattern($patternText, $target)) {
356 + return true;
357 + }
358 + }
359 +
360 + return false;
361 + }
362 +
363 + private function matchPattern(string $pattern, string $target): bool
364 + {
365 + if (function_exists('fnmatch')) {
366 + return fnmatch($pattern, $target);
367 + }
368 +
369 + $regex = '/^' . str_replace(array('\*', '\?'), array('.*', '.'), preg_quote($pattern, '/')) . '$/i';
370 + return preg_match($regex, $target) === 1;
371 + }
372 +
373 + private function isPassedSampling(): bool
374 + {
375 + $sampleRate = (float) config('query_log.sample_rate', 100);
376 + if ($sampleRate >= 100) {
377 + return true;
378 + }
379 + if ($sampleRate <= 0) {
380 + return false;
381 + }
382 +
383 + $random = mt_rand(1, 10000) / 100;
384 + return $random <= $sampleRate;
385 + }
386 +
387 + private function isInTransaction(Connection $connection): bool
388 + {
389 + return method_exists($connection, 'transactionLevel') && $connection->transactionLevel() > 0;
390 + }
391 +
392 + private function shouldLogInCurrentRuntime(): bool
393 + {
394 + if (app()->runningInConsole()) {
395 + return (bool) config('query_log.log_on_console', false);
396 + }
397 +
398 + return true;
399 + }
400 +
401 + private function isEnabled(): bool
402 + {
403 + return (bool) config('query_log.enable', false);
404 + }
405 +
406 + private function registerPublishableResources(): void
407 + {
408 + $this->publishes(
409 + array(
410 + __DIR__ . '/../config/query_log.php' => config_path('query_log.php'),
411 + ),
412 + 'query-log-config'
413 + );
414 +
415 + $this->publishes(
416 + array(
417 + __DIR__ . '/../database/migrations/2026_02_20_000000_create_log_queries_table.php'
418 + => database_path('migrations/' . date('Y_m_d_His') . '_create_log_queries_table.php'),
419 + ),
420 + 'query-log-migrations'
421 + );
422 + }
423 +}
1 +<?php
2 +return [
3 + 'enable' => env('ENABLE_QUERY_LOG', false),
4 + 'log_on_console' => env('QUERY_LOG_ON_CONSOLE', false),
5 + 'sample_rate' => env('QUERY_LOG_SAMPLE_RATE', 100), // 0-100 (%)
6 + 'chunk' => env('QUERY_LOG_CHUNK', 200),
7 + 'max_queries_per_request' => env('QUERY_LOG_MAX_PER_REQUEST', 1000),
8 + 'min_time' => env('QUERY_LOG_MIN_TIME', 0),
9 + 'connection' => env('QUERY_LOG_CONNECTION', 'query_log'),
10 + 'table' => env('QUERY_LOG_TABLE', 'log_queries'),
11 + 'queue_connection' => env('QUERY_LOG_QUEUE_CONNECTION', null),
12 + 'queue_name' => env('QUERY_LOG_QUEUE_NAME', null),
13 + 'max_sql_length' => env('QUERY_LOG_MAX_SQL_LENGTH', 4000),
14 + 'mask_sensitive_bindings' => env('QUERY_LOG_MASK_SENSITIVE_BINDINGS', true),
15 + 'sensitive_keywords' => array(
16 + 'password',
17 + 'passwd',
18 + 'pwd',
19 + 'token',
20 + 'secret',
21 + 'api_key',
22 + 'apikey',
23 + 'authorization',
24 + 'cookie',
25 + 'credit_card',
26 + 'card_number',
27 + 'cvv',
28 + 'pin',
29 + ),
30 + 'masked_value' => '***',
31 + 'skip_route_patterns' => array(
32 + 'horizon*',
33 + 'telescope*',
34 + '_debugbar*',
35 + ),
36 + 'skip_command_patterns' => array(
37 + 'queue:*',
38 + 'horizon*',
39 + 'schedule:run',
40 + ),
41 + 'ignore_tables' => array(
42 + 'log_queries',
43 + 'jobs',
44 + 'failed_jobs',
45 + 'migrations',
46 + 'mst_batch',
47 + ),
48 +];
1 +<?php
2 +
3 +use Illuminate\Database\Migrations\Migration;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Support\Facades\Schema;
6 +
7 +return new class extends Migration {
8 + public function up(): void
9 + {
10 + $connection = config('query_log.connection', 'query_log');
11 + $table = config('query_log.table', 'log_queries');
12 +
13 + Schema::connection($connection)->create($table, function (Blueprint $table) {
14 + $table->bigIncrements('id');
15 + $table->string('action', 512)->nullable();
16 + $table->longText('query');
17 + $table->string('query_type', 32);
18 + $table->decimal('query_time', 10, 3)->default(0);
19 + $table->dateTime('query_at');
20 + $table->unsignedInteger('query_order')->default(0);
21 + $table->string('connection', 64)->nullable();
22 + $table->string('ip', 64)->nullable();
23 + $table->string('user_id', 64)->nullable();
24 + $table->boolean('is_screen')->default(false);
25 +
26 + $table->index('query_at', 'idx_log_queries_query_at');
27 + $table->index('query_type', 'idx_log_queries_query_type');
28 + $table->index('connection', 'idx_log_queries_connection');
29 + $table->index('user_id', 'idx_log_queries_user_id');
30 + $table->index(array('query_at', 'query_type'), 'idx_log_queries_at_type');
31 + $table->index(array('connection', 'query_at'), 'idx_log_queries_connection_at');
32 + });
33 + }
34 +
35 + public function down(): void
36 + {
37 + $connection = config('query_log.connection', 'query_log');
38 + $table = config('query_log.table', 'log_queries');
39 +
40 + Schema::connection($connection)->dropIfExists($table);
41 + }
42 +};
1 +<?php
2 +
3 +require __DIR__ . '/vendor/autoload.php';
4 +
5 +$container = new Illuminate\Container\Container();
6 +Illuminate\Container\Container::setInstance($container);
7 +
8 +$container->instance('config', new Illuminate\Config\Repository(array(
9 + 'barcode' => array(
10 + 'store_path' => sys_get_temp_dir(),
11 + ),
12 +)));
13 +
14 +$base64 = Daito\Lib\DaitoBarcode::generateBarcodeQrCode('HELLO-QR');
15 +
16 +echo substr($base64, 0, 80) . PHP_EOL;
17 +echo 'length: ' . strlen($base64) . PHP_EOL;
18 +
19 +echo Daito\Lib\DaitoMath::div(1, 2) . PHP_EOL;
...\ No newline at end of file ...\ No newline at end of file