Showing
12 changed files
with
1255 additions
and
7 deletions
| ... | @@ -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": { | ... | ... |
src/DaitoBarcode.php
0 → 100644
| 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 |
src/DaitoNumber.php
0 → 100644
| 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 | ... | ... |
src/QueryLog/Jobs/SaveQueryLogJob.php
0 → 100644
| 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 | +} |
src/QueryLog/Models/QueryLog.php
0 → 100644
| 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 | +} |
src/QueryLog/Providers/QueryLogProvider.php
0 → 100644
| 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 | +} |
src/QueryLog/config/query_log.php
0 → 100644
| 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 | +}; |
test.php
0 → 100644
| 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 |
-
Please register or sign in to post a comment