This commit is contained in:
aarsyah0 2025-05-27 10:51:21 +07:00
parent b15a1f6a46
commit eb2ac3c16d
93 changed files with 16682 additions and 555 deletions

View File

@ -3,57 +3,135 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Response;
class HomeController extends Controller
{
public function home()
/**
* Display the dashboard with sentiment counts.
*/
public function index()
{
return redirect('dashboard');
}
// Paths to CSV files in public/storage
$pathDana = public_path('storage/data_labeled.csv');
$pathGopay = public_path('storage/data_labeledgopay.csv');
$pathShopee = public_path('storage/data_labeledshopee.csv');
public function wordcloudData(Request $request)
{
$source = $request->query('source', 'gopay'); // default 'gopay'
// Read CSVs
$danaRows = $this->readCsv($pathDana);
$gopayRows = $this->readCsv($pathGopay);
$shopeeRows = $this->readCsv($pathShopee);
// Tentukan file berdasarkan sumber
if ($source === 'gopay') {
$files = ['gopaylabel.csv'];
} elseif ($source === 'dana') {
$files = ['danalabel.csv'];
} elseif($source === 'shopeepay'){
$files = ['shopeepaylabel.csv'];
} else {
$files = ['gopaylabel.csv', 'danalabel.csv', 'shopeepaylabel.csv'];
}
// Merge for "all"
$allRows = array_merge($danaRows, $gopayRows, $shopeeRows);
$allText = '';
foreach ($files as $file) {
$path = resource_path("views/{$file}");
if (File::exists($path)) {
// Baca baris per baris
$csv = array_map('str_getcsv', File::lines($path)->toArray());
$header = array_shift($csv);
$idx = array_search('cleaned_text', $header);
foreach ($csv as $row) {
if (isset($row[$idx])) {
$allText .= ' '.strtolower($row[$idx]);
// Counting helper
$calcCounts = function(array $rows) {
$counts = ['positif' => 0, 'netral' => 0, 'negatif' => 0];
$map = [
'positive' => 'positif',
'neutral' => 'netral',
'negative' => 'negatif',
];
foreach ($rows as $r) {
$label = strtolower(trim($r['label_auto'] ?? ''));
if (isset($map[$label])) {
$counts[$map[$label]]++;
}
}
return $counts;
};
// Calculate counts
$counts = [
'all' => $calcCounts($allRows),
'dana' => $calcCounts($danaRows),
'gopay' => $calcCounts($gopayRows),
'shopeepay' => $calcCounts($shopeeRows),
];
return view('dashboard', compact('counts'));
}
/**
* Return JSON word-cloud data for a given source.
* GET /wordcloud-data?source=all|dana|gopay|shopeepay
*/
public function wordcloudData(Request $request)
{
$source = strtolower($request->query('source', 'all'));
$sentiment = strtolower($request->query('sentiment', 'all'));
$map = ['positif'=>'positive','netral'=>'neutral','negatif'=>'negative'];
// Ambil semua baris sesuai source (sama seperti sebelumnya)
switch ($source) {
case 'dana':
$rows = $this->readCsv(public_path('storage/data_labeled.csv'));
break;
case 'gopay':
$rows = $this->readCsv(public_path('storage/data_labeledgopay.csv'));
break;
case 'shopeepay':
$rows = $this->readCsv(public_path('storage/data_labeledshopee.csv'));
break;
default:
$rows = array_merge(
$this->readCsv(public_path('storage/data_labeled.csv')),
$this->readCsv(public_path('storage/data_labeledgopay.csv')),
$this->readCsv(public_path('storage/data_labeledshopee.csv'))
);
}
// Jika permintaan untuk sentimen tertentu, filter dulu
if (isset($map[$sentiment])) {
$engLabel = $map[$sentiment];
$rows = array_filter($rows, fn($r) => strtolower($r['label_auto'] ?? '') === $engLabel);
}
// Tokenize & hitung frekuensi kata
$freq = [];
foreach ($rows as $row) {
$words = preg_split('/\W+/', strtolower($row['clean_text'] ?? ''), -1, PREG_SPLIT_NO_EMPTY);
foreach ($words as $w) {
if (strlen($w) < 3) continue;
$freq[$w] = ($freq[$w] ?? 0) + 1;
}
}
$words = str_word_count($allText, 1);
$filtered = array_filter($words, fn($w) => strlen($w) > 2);
$counts = array_count_values($filtered);
arsort($counts);
$top100 = array_slice($counts, 0, 100, true);
$list = [];
foreach ($top100 as $word => $count) {
$list[] = [$word, $count];
arsort($freq);
$top = array_slice($freq, 0, 800, true); // batasi ke 70 kata
$list = [];
foreach ($top as $word => $count) {
$list[] = [$word, $count];
}
return Response::json($list);
}
/**
* Helper: load a CSV into an associative array.
*/
private function readCsv(string $path): array
{
if (! File::exists($path)) {
logger()->warning("CSV file not found at {$path}");
return [];
}
return response()->json($list);
$handle = fopen($path, 'r');
$header = fgetcsv($handle);
$data = [];
while ($row = fgetcsv($handle)) {
$data[] = array_combine($header, $row);
}
fclose($handle);
return $data;
}
}

View File

@ -12,7 +12,8 @@
"guzzlehttp/guzzle": "^7.4",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.6"
"laravel/tinker": "^2.6",
"league/csv": "^9.23"
},
"require-dev": {
"spatie/laravel-ignition": "^2.0",

93
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "44af905ee6d3a491d075107e62a3ab1e",
"content-hash": "9a808cd3140036bb203b0f5ab49fbb19",
"packages": [
{
"name": "brick/math",
@ -1708,6 +1708,97 @@
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/csv",
"version": "9.23.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "774008ad8a634448e4f8e288905e070e8b317ff3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/774008ad8a634448e4f8e288905e070e8b317ff3",
"reference": "774008ad8a634448e4f8e288905e070e8b317ff3",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^8.1.2"
},
"require-dev": {
"ext-dom": "*",
"ext-xdebug": "*",
"friendsofphp/php-cs-fixer": "^3.69.0",
"phpbench/phpbench": "^1.4.0",
"phpstan/phpstan": "^1.12.18",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
"phpunit/phpunit": "^10.5.16 || ^11.5.7",
"symfony/var-dumper": "^6.4.8 || ^7.2.3"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters",
"ext-mysqli": "Requiered to use the package with the MySQLi extension",
"ext-pdo": "Required to use the package with the PDO extension",
"ext-pgsql": "Requiered to use the package with the PgSQL extension",
"ext-sqlite3": "Required to use the package with the SQLite3 extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"League\\Csv\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "CSV data manipulation made easy in PHP",
"homepage": "https://csv.thephpleague.com",
"keywords": [
"convert",
"csv",
"export",
"filter",
"import",
"read",
"transform",
"write"
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2025-03-28T06:52:04+00:00"
},
{
"name": "league/flysystem",
"version": "3.29.1",

1
public/storage Symbolic link
View File

@ -0,0 +1 @@
/Users/sutanarsyahnugraha/Documents/Joki/analisis/storage/app/public

View File

@ -1,538 +1,258 @@
@extends('layouts/user_type/auth')
@section('content')
@php
if (!function_exists('getSentimentCounts')) {
function getSentimentCounts($path)
{
if (!file_exists($path)) {
return ['positif' => 0, 'netral' => 0, 'negatif' => 0];
}
$rows = array_map('str_getcsv', file($path));
array_shift($rows);
$labels = array_column($rows, 1);
$counts = array_count_values($labels);
return array_merge(['positif' => 0, 'netral' => 0, 'negatif' => 0], $counts);
}
}
// Hitung data sekali
$base = resource_path('views');
$cDana = getSentimentCounts("{$base}/danalabel.csv");
$cGoPay = getSentimentCounts("{$base}/gopaylabel.csv");
$cShopee = getSentimentCounts("{$base}/shopeepaylabel.csv");
// Total kartu (opsional)
$totalPositif = $cDana['positif'] + $cGoPay['positif'] + $cShopee['positif'];
$totalNetral = $cDana['netral'] + $cGoPay['netral'] + $cShopee['netral'];
$totalNegatif = $cDana['negatif'] + $cGoPay['negatif'] + $cShopee['negatif'];
@endphp
{{-- Ringkasan Sentimen --}}
<div class="row mb-4">
<div class="col-12">
<!-- Filter di atas, rata-kanan -->
<div class="d-flex justify-content-end mb-3">
<select id="walletFilter" class="form-select form-select-sm w-auto" style="min-width: 120px;">
<select id="walletFilter" class="form-select form-select-sm w-auto" style="min-width:120px;">
<option value="all">Semua</option>
<option value="dana">Dana</option>
<option value="gopay">GoPay</option>
<option value="shopee">ShopeePay</option>
<option value="shopeepay">ShopeePay</option>
</select>
</div>
<!-- Baris kartu -->
<div class="row gx-3">
<div class="col-md-4">
<div class="card text-white shadow-lg border-radius-xl"
style="background-color: #b22fa4; color: #fff; box-shadow: inset 2px 2px 6px rgba(255, 255, 255, 0.1), 6px 6px 20px rgba(178, 47, 164, 0.5);">
<div class="card-body d-flex justify-content-between align-items-center p-4">
<div>
<p class="mb-1 text-sm fw-bold opacity-85">Total Positif</p>
<h4 id="positifCount" class="mb-0 text-white fw-bolder">
{{ $cDana['positif'] + $cGoPay['positif'] + $cShopee['positif'] }}
</h4>
</div>
<div class="d-flex align-items-center justify-content-center rounded-circle"
style="width: 64px; height: 64px; background-color: rgba(255, 255, 255, 0.15); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);">
<i class="fa-solid fa-face-smile fa-2x opacity-9"
style="transition: transform 0.3s ease, opacity 0.3s ease;"
onmouseover="this.style.transform='scale(1.2)'; this.style.opacity='1';"
onmouseout="this.style.transform='scale(1)'; this.style.opacity='0.9';"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white"
style="background-color: #a95b91; color: #fff; box-shadow: inset 2px 2px 6px rgba(255, 255, 255, 0.1), 6px 6px 20px rgba(178, 47, 164, 0.5);">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<p class="mb-1 text-sm">Total Netral</p>
<h4 id="netralCount" class="mb-0 text-white">
{{ $cDana['netral'] + $cGoPay['netral'] + $cShopee['netral'] }}
</h4>
</div>
<div class="d-flex align-items-center justify-content-center rounded-circle"
style="width: 64px; height: 64px; background-color: rgba(255, 255, 255, 0.15); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);">
<i class="fa-solid fa-face-meh fa-2x opacity-9"
style="transition: transform 0.3s ease, opacity 0.3s ease;"
onmouseover="this.style.transform='scale(1.2)'; this.style.opacity='1';"
onmouseout="this.style.transform='scale(1)'; this.style.opacity='0.9';"></i>
@foreach (['positif' => 'bg-success', 'netral' => 'bg-warning', 'negatif' => 'bg-danger'] as $sent => $badge)
<div class="col-md-4">
<div class="card h-100 text-white {{ $badge }} shadow-lg rounded-2">
<div class="card-body d-flex justify-content-between align-items-center p-4">
<div>
<p class="mb-1 text-sm fw-bold text-uppercase">Total {{ ucfirst($sent) }}</p>
<h4 id="{{ $sent }}Count" class="mb-0 fw-bolder">
{{ $counts['all'][$sent] }}
</h4>
</div>
<div class="rounded-circle d-flex justify-content-center align-items-center"
style="width:64px; height:64px; background:rgba(255,255,255,0.15);">
<i class="fa-solid fa-face-{{ $sent === 'positif' ? 'smile' : 'frown' }} fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white"
style="background-color: #8b2f5e; color: #fff; box-shadow: inset 2px 2px 6px rgba(255, 255, 255, 0.1), 6px 6px 20px rgba(178, 47, 164, 0.5);">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<p class="mb-1 text-sm">Total Negatif</p>
<h4 id="negatifCount" class="mb-0 text-white">
{{ $cDana['negatif'] + $cGoPay['negatif'] + $cShopee['negatif'] }}
</h4>
</div>
<div class="d-flex align-items-center justify-content-center rounded-circle"
style="width: 64px; height: 64px; background-color: rgba(255, 255, 255, 0.15); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);">
<i class="fa-solid fa-face-frown fa-2x opacity-9"
style="transition: transform 0.3s ease, opacity 0.3s ease;"
onmouseover="this.style.transform='scale(1.2)'; this.style.opacity='1';"
onmouseout="this.style.transform='scale(1)'; this.style.opacity='0.9';"></i>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
<div class="row mt-4">
<!-- Bar Chart Card -->
<div class="col-lg-5 mb-lg-0 mb-4">
<div class="card z-index-2">
<div class="card-header pb-0">
<h6 class="mb-1">Perbandingan Sentimen E-Wallet</h6>
<p class="text-sm">Grafik batang menunjukkan jumlah sentimen positif, netral, dan negatif untuk Dana,
GoPay, dan ShopeePay.</p>
{{-- Charts Section --}}
<div class="row mt-4 align-items-stretch">
<div class="col-lg-5 mb-4">
<div class="card h-100">
<div class="card-header">
<h6>Perbandingan Sentimen E-Wallet</h6>
</div>
<div class="card-body p-3">
<div class="bg-gradient-light border-radius-lg py-3 pe-1 mb-3">
<div class="chart">
<canvas id="chart-bars" class="chart-canvas" height="250" style="width:100%;"></canvas>
</div>
</div>
<div class="card-body p-0" style="height:300px;">
<canvas id="chart-bars" width="400" height="300"></canvas>
</div>
</div>
</div>
<!-- Line Chart Card -->
<div class="col-lg-7 mb-lg-0 mb-4">
<div class="card z-index-2">
<div class="card-header pb-0">
<h6 class="mb-1">Tren Sentimen E-Wallet</h6>
<p class="text-sm">Garis menunjukkan perubahan proporsi sentimen (positif, netral, negatif) di setiap
e-wallet.</p>
<div class="col-lg-7 mb-4">
<div class="card h-100">
<div class="card-header">
<h6>Tren Sentimen E-Wallet</h6>
</div>
<div class="card-body p-3">
<div class="chart">
<canvas id="chart-line" class="chart-canvas" height="318" style="width:100%;"></canvas>
</div>
<div class="card-body p-0" style="height:300px;">
<canvas id="chart-line" width="700" height="300"></canvas>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<!-- Pie Chart Card with filter -->
<div class="col-lg-5 mb-lg-0 mb-4 d-flex">
<div class="card z-index-2 flex-grow-1">
<div class="card-header pb-0 d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">Distribusi Persentase Sentimen</h6>
<p class="text-sm">Grafik pai menampilkan persentase sentimen (positif, netral, negatif) untuk
e-wallet terpilih.</p>
</div>
<select id="wallet-filter" class="form-select form-select-sm w-auto">
<option value="all">Semua E-Wallet</option>
{{-- Pie + Table --}}
<div class="row mt-4 align-items-stretch">
<div class="col-lg-5 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6>Distribusi Persentase Sentimen</h6>
<select id="walletFilterPie" class="form-select form-select-sm w-auto" style="min-width:120px;">
<option value="all">Semua</option>
<option value="dana">Dana</option>
<option value="gopay">GoPay</option>
<option value="shopeepay">ShopeePay</option>
</select>
</div>
<div class="card-body p-3">
<div class="chart">
<canvas id="chart-pie" class="chart-canvas" height="250" style="width:100%;"></canvas>
</div>
<div class="card-body p-0" style="height:300px;">
<canvas id="chart-pie" width="400" height="300"></canvas>
</div>
</div>
</div>
<div class="col-lg-7 d-flex">
<div class="card z-index-2 flex-grow-1">
<div class="card-header pb-0">
<div class="col-lg-7 mb-4">
<div class="card h-100">
<div class="card-header">
<h6>Perbandingan Sentimen Antar Brand</h6>
<p class="text-sm">Tabel ini menunjukkan jumlah dan proporsi sentimen untuk masing-masing e-wallet.</p>
</div>
<div class="card-body px-3 pt-0 pb-3">
<div class="table-responsive">
<table class="table align-items-center mb-0">
<thead>
<div class="card-body p-0" style="overflow-y:auto; max-height:300px;">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>E-Wallet</th>
<th>Positif</th>
<th>Netral</th>
<th>Negatif</th>
<th>Net Score</th>
</tr>
</thead>
<tbody>
@foreach (['Dana' => 'dana', 'GoPay' => 'gopay', 'ShopeePay' => 'shopeepay'] as $label => $key)
@php
$d = $counts[$key];
$net = (($d['positif'] - $d['negatif']) / max(1, array_sum($d))) * 100;
@endphp
<tr>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
style="background-color: #f8f9fa;">E-Wallet</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
style="background-color: #f8f9fa;">Positif</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
style="background-color: #f8f9fa;">Netral</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
style="background-color: #f8f9fa;">Negatif</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
style="background-color: #f8f9fa;">Net Score</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>DANA</strong></td>
<td>{{ $cDana['positif'] }}</td>
<td>{{ $cDana['netral'] }}</td>
<td>{{ $cDana['negatif'] }}</td>
<td><strong>{{ $label }}</strong></td>
<td>{{ $d['positif'] }}</td>
<td>{{ $d['netral'] }}</td>
<td>{{ $d['negatif'] }}</td>
<td>
<span
class="badge {{ $cDana['positif'] - $cDana['negatif'] > 0 ? 'bg-success' : 'bg-danger' }}">
{{ number_format((($cDana['positif'] - $cDana['negatif']) / max(1, $cDana['positif'] + $cDana['netral'] + $cDana['negatif'])) * 100, 1) }}%
<span class="badge {{ $net > 0 ? 'bg-success' : 'bg-danger' }}">
{{ number_format($net, 1) }}%
</span>
</td>
</tr>
<tr>
<td><strong>GoPay</strong></td>
<td>{{ $cGoPay['positif'] }}</td>
<td>{{ $cGoPay['netral'] }}</td>
<td>{{ $cGoPay['negatif'] }}</td>
<td>
<span
class="badge {{ $cGoPay['positif'] - $cGoPay['negatif'] > 0 ? 'bg-success' : 'bg-danger' }}">
{{ number_format((($cGoPay['positif'] - $cGoPay['negatif']) / max(1, $cGoPay['positif'] + $cGoPay['netral'] + $cGoPay['negatif'])) * 100, 1) }}%
</span>
</td>
</tr>
<tr>
<td><strong>ShopeePay</strong></td>
<td>{{ $cShopee['positif'] }}</td>
<td>{{ $cShopee['netral'] }}</td>
<td>{{ $cShopee['negatif'] }}</td>
<td>
<span
class="badge {{ $cShopee['positif'] - $cShopee['negatif'] > 0 ? 'bg-success' : 'bg-danger' }}">
{{ number_format((($cShopee['positif'] - $cShopee['negatif']) / max(1, $cShopee['positif'] + $cShopee['netral'] + $cShopee['negatif'])) * 100, 1) }}%
</span>
</td>
</tr>
</tbody>
</table>
</div>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="center d-flex flex-column justify-content-center align-items-center"
style="padding: 2rem; min-height: 100vh;">
<div class="row justify-content-center my-5">
<div class="col-lg-8">
<div class="mb-3" style="width: 300px;">
<select id="sourceSelect" class="form-select">
<option value="gopay" selected>Gopay</option>
<option value="dana">Dana</option>
<option value="shopeepay">Shopeepay</option>
<option value="gabungan">Gabungan</option>
</select>
</div>
<div class="card shadow-lg border-radius-lg bg-white">
<div class="card-header pb-2">
{{-- WordCloud Section --}}
<div class="row mt-5">
<div class="col-12 mb-3 d-flex justify-content-center">
<select id="sourceSelect" class="form-select w-auto" style="min-width:120px;">
<option value="all">Gabungan</option>
<option value="dana">Dana</option>
<option value="gopay">GoPay</option>
<option value="shopeepay">ShopeePay</option>
</select>
</div>
@foreach (['positif', 'netral', 'negatif'] as $s)
<div class="col-md-4">
<div class="card h-100">
<div class="card-header text-center">
<h6 class="mb-0">{{ ucfirst($s) }}</h6>
</div>
<div class="card-body d-flex justify-content-center align-items-center py-4">
<canvas id="wc" width="1800" height="800"
style="max-width: 100%; height: auto;"></canvas>
</div>
<div class="card-footer text-muted text-end small">
Generated on {{ date('d F Y, H:i') }}
<div class="card-body d-flex justify-content-center align-items-center p-0" style="height:250px;">
<canvas id="wc-{{ $s }}" style="width:100%; height:100%;"></canvas>
</div>
</div>
</div>
</div>
@endforeach
</div>
@endsection
@push('dashboard')
<!-- Load Chart.js library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/wordcloud2@1.1.2/dist/wordcloud2.js"></script>
<script>
window.onload = function() {
// Chart contexts
var ctxBar = document.getElementById("chart-bars").getContext("2d");
var ctxLine = document.getElementById("chart-line").getContext("2d");
var ctxPie = document.getElementById("chart-pie").getContext("2d");
var filter = document.getElementById("wallet-filter");
document.addEventListener('DOMContentLoaded', () => {
const counts = @json($counts);
// Summary
const upd = key => ['positif', 'netral', 'negatif']
.forEach(s => document.getElementById(s + 'Count').textContent = counts[key][s]);
document.getElementById('walletFilter').addEventListener('change', e => upd(e.target.value));
upd('all');
@php
function getSentimentCounts($path)
{
if (!file_exists($path)) {
return ['positif' => 0, 'netral' => 0, 'negatif' => 0];
}
$rows = array_map('str_getcsv', file($path));
array_shift($rows);
$labels = array_column($rows, 1);
$counts = array_count_values($labels);
return array_merge(['positif' => 0, 'netral' => 0, 'negatif' => 0], $counts);
}
$base = resource_path('views');
$cDana = getSentimentCounts("{$base}/danalabel.csv");
$cGoPay = getSentimentCounts("{$base}/gopaylabel.csv");
$cShopee = getSentimentCounts("{$base}/shopeepaylabel.csv");
@endphp
const COLORS = ['rgba(75,192,192,0.6)', 'rgba(54,162,235,0.6)', 'rgba(255,206,86,0.6)'];
const BORDERS = ['rgba(75,192,192,1)', 'rgba(54,162,235,1)', 'rgba(255,206,86,1)'];
// Data arrays
var labels = ['Positif', 'Netral', 'Negatif'];
var danaData = [{{ $cDana['positif'] }}, {{ $cDana['netral'] }}, {{ $cDana['negatif'] }}];
var goPayData = [{{ $cGoPay['positif'] }}, {{ $cGoPay['netral'] }}, {{ $cGoPay['negatif'] }}];
var shopeeData = [{{ $cShopee['positif'] }}, {{ $cShopee['netral'] }}, {{ $cShopee['negatif'] }}];
var allData = [
danaData[0] + goPayData[0] + shopeeData[0],
danaData[1] + goPayData[1] + shopeeData[1],
danaData[2] + goPayData[2] + shopeeData[2]
];
// Bar chart
var gradBar = function(ctx, color) {
var g = ctx.createLinearGradient(0, 0, 0, 250);
g.addColorStop(0, color + '0.8)');
g.addColorStop(1, color + '0.2)');
return g;
};
new Chart(ctxBar, {
// Bar
new Chart('chart-bars', {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Dana',
data: danaData,
backgroundColor: gradBar(ctxBar, 'rgba(75,192,192,'),
borderRadius: 4,
maxBarThickness: 20
},
{
label: 'GoPay',
data: goPayData,
backgroundColor: gradBar(ctxBar, 'rgba(255,159,64,'),
borderRadius: 4,
maxBarThickness: 20
},
{
label: 'ShopeePay',
data: shopeeData,
backgroundColor: gradBar(ctxBar, 'rgba(153,102,255,'),
borderRadius: 4,
maxBarThickness: 20
}
]
labels: ['Positif', 'Netral', 'Negatif'],
datasets: ['dana', 'gopay', 'shopeepay'].map((k, i) => ({
label: k.charAt(0).toUpperCase() + k.slice(1),
data: ['positif', 'netral', 'negatif'].map(s => counts[k][s]),
backgroundColor: COLORS[i],
borderColor: BORDERS[i],
borderWidth: 1
}))
},
options: {
responsive: true,
animation: {
duration: 1200,
easing: 'easeInOutQuad'
},
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
legend: {
position: 'top'
}
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
// Line chart
function makeLineGrad(ctx, base) {
var g = ctx.createLinearGradient(0, 0, 0, 250);
g.addColorStop(0, base + '0.8)');
g.addColorStop(1, base + '0.2)');
return g;
}
new Chart(ctxLine, {
// Line
new Chart('chart-line', {
type: 'line',
data: {
labels: ['Dana', 'GoPay', 'Shopeepay'],
datasets: [{
label: 'Positif',
data: [danaData[0], goPayData[0], shopeeData[0]],
borderColor: makeLineGrad(ctxLine, 'rgba(54,162,235,'),
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 3
},
{
label: 'Netral',
data: [danaData[1], goPayData[1], shopeeData[1]],
borderColor: 'rgba(255,206,86,0.8)',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 3
},
{
label: 'Negatif',
data: [danaData[2], goPayData[2], shopeeData[2]],
borderColor: 'rgba(255,99,132,0.8)',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 3
}
]
labels: ['Dana', 'GoPay', 'ShopeePay'],
datasets: ['positif', 'netral', 'negatif'].map((s, i) => ({
label: s.charAt(0).toUpperCase() + s.slice(1),
data: ['dana', 'gopay', 'shopeepay'].map(k => counts[k][s]),
borderColor: BORDERS[i],
fill: false,
tension: 0.4
}))
},
options: {
responsive: true,
animation: {
duration: 1200,
easing: 'easeOutQuart'
},
scales: {
y: {
beginAtZero: true
}
},
plugins: {
legend: {
position: 'bottom'
}
}
maintainAspectRatio: false
}
});
// Pie chart with tooltip percentages
var pieChart = new Chart(ctxPie, {
// Pie
const pie = new Chart('chart-pie', {
type: 'pie',
data: {
labels: labels,
labels: ['Positif', 'Netral', 'Negatif'],
datasets: [{
data: allData,
backgroundColor: ['#4BC0C0', '#FFCE56', '#FF6384'],
hoverOffset: 10
data: ['positif', 'netral', 'negatif'].map(s => counts.all[s]),
backgroundColor: COLORS,
borderColor: BORDERS,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // Disable maintain aspect ratio
aspectRatio: 2, // Aspect ratio of 1 will give a square chart, you can adjust to your needs
animation: {
animateRotate: true,
duration: 1000
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
var value = context.parsed;
var sum = context.chart.data.datasets[0].data.reduce(function(a, b) {
return a + b;
}, 0);
var pct = Math.round(value * 100 / sum);
return context.label + ': ' + value + ' (' + pct + '%)';
}
}
},
legend: {
position: 'bottom'
}
}
maintainAspectRatio: false
}
});
// Filter event for pie chart
filter.addEventListener('change', function() {
var m = this.value;
pieChart.data.datasets[0].data = (m === 'dana' ? danaData : m === 'gopay' ? goPayData : m ===
'shopeepay' ? shopeeData : allData);
pieChart.update();
document.getElementById('walletFilterPie').addEventListener('change', e => {
pie.data.datasets[0].data = ['positif', 'netral', 'negatif'].map(s => counts[e.target.value]
[s]);
pie.update();
});
};
document.addEventListener('DOMContentLoaded', () => {
const filter = document.getElementById('walletFilter');
const positifEl = document.getElementById('positifCount');
const netralEl = document.getElementById('netralCount');
const negatifEl = document.getElementById('negatifCount');
const data = {
all: {
positif: {{ $cDana['positif'] + $cGoPay['positif'] + $cShopee['positif'] }},
netral: {{ $cDana['netral'] + $cGoPay['netral'] + $cShopee['netral'] }},
negatif: {{ $cDana['negatif'] + $cGoPay['negatif'] + $cShopee['negatif'] }},
},
dana: {
positif: {{ $cDana['positif'] }},
netral: {{ $cDana['netral'] }},
negatif: {{ $cDana['negatif'] }},
},
gopay: {
positif: {{ $cGoPay['positif'] }},
netral: {{ $cGoPay['netral'] }},
negatif: {{ $cGoPay['negatif'] }},
},
shopee: {
positif: {{ $cShopee['positif'] }},
netral: {{ $cShopee['netral'] }},
negatif: {{ $cShopee['negatif'] }},
},
};
const sourceSel = document.getElementById('sourceSelect');
const sentiments = ['positif', 'netral', 'negatif'];
filter.addEventListener('change', function() {
const d = data[this.value];
positifEl.textContent = d.positif;
netralEl.textContent = d.netral;
negatifEl.textContent = d.negatif;
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/wordcloud@1.1.2/src/wordcloud2.min.js" defer></script>
<script defer>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('wc');
const select = document.getElementById('sourceSelect');
function drawWC(sentiment) {
const cvs = document.getElementById(`wc-${sentiment}`);
// gunakan koordinat canvas internal
const origin = [cvs.width / 2, cvs.height / 2];
function loadWordCloud(source) {
fetch(`/wordcloud-data?source=${source}`)
.then(res => res.json())
fetch(`/wordcloud-data?source=${sourceSel.value}&sentiment=${sentiment}`)
.then(r => r.json())
.then(list => {
console.log('WordCloud data:', list); // → pastikan ada data
WordCloud(canvas, {
WordCloud(cvs, {
list,
gridSize: Math.round(1600 / 100),
weightFactor: w => Math.pow(w, 0.5) * 8,
fontFamily: 'Arial, sans-serif',
rotateRatio: 0,
shape: 'elliptic',
ellipticity: 0.65,
drawOutOfBound: false,
backgroundColor: '#ffffff'
clearCanvas: true,
weightFactor: 1,
gridSize: 2,
rotateRatio: 0.3,
backgroundColor: window.getComputedStyle(cvs).backgroundColor,
origin
});
})
.catch(err => console.error('WC fetch err:', err));
});
}
// Load pertama kali pakai nilai default di <select>
loadWordCloud(select.value);
select.addEventListener('change', () => {
loadWordCloud(select.value);
});
sourceSel.addEventListener('change', () => sentiments.forEach(drawWC));
sentiments.forEach(drawWC);
});
</script>
@endpush

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -87,6 +87,7 @@
<script async defer src="https://buttons.github.io/buttons.js"></script>
<!-- Control Center for Soft Dashboard: parallax effects, scripts for the example pages etc -->
<script src="../assets/js/soft-ui-dashboard.min.js?v=1.0.3"></script>
@stack('scripts')
</body>
</html>

View File

@ -6,84 +6,41 @@ use App\Http\Controllers\InfoUserController;
use App\Http\Controllers\RegisterController;
use App\Http\Controllers\ResetController;
use App\Http\Controllers\SessionsController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::group(['middleware' => 'auth'], function () {
Route::get('/', [HomeController::class, 'home']);
Route::get('dashboard', function () {
return view('dashboard');
})->name('dashboard');
// dashboard → HomeController@index (formerly HomeController@home)
Route::get('/', [HomeController::class, 'index'])->name('dashboard');
Route::get('/dashboard', [HomeController::class, 'index']);
Route::get('billing', function () {
return view('billing');
})->name('billing');
// word-cloud JSON endpoint
Route::get('/wordcloud-data', [HomeController::class, 'wordcloudData']);
Route::get('profile', function () {
return view('profile');
})->name('profile');
// other authenticated routes…
Route::get('billing', fn() => view('billing'))->name('billing');
Route::get('profile', fn() => view('profile'))->name('profile');
Route::get('rtl', fn() => view('rtl'))->name('rtl');
Route::get('user-management', fn() => view('laravel-examples/user-management'))->name('user-management');
Route::get('tables', fn() => view('tables'))->name('tables');
Route::get('virtual-reality', fn() => view('virtual-reality'))->name('virtual-reality');
Route::get('static-sign-in', fn() => view('static-sign-in'))->name('sign-in');
Route::get('static-sign-up', fn() => view('static-sign-up'))->name('sign-up');
Route::get('rtl', function () {
return view('rtl');
})->name('rtl');
Route::get('user-management', function () {
return view('laravel-examples/user-management');
})->name('user-management');
Route::get('tables', function () {
return view('tables');
})->name('tables');
Route::get('virtual-reality', function () {
return view('virtual-reality');
})->name('virtual-reality');
Route::get('static-sign-in', function () {
return view('static-sign-in');
})->name('sign-in');
Route::get('static-sign-up', function () {
return view('static-sign-up');
})->name('sign-up');
Route::get('/logout', [SessionsController::class, 'destroy']);
Route::get('/data-sentimen', [InfoUserController::class, 'create']);
Route::post('/data-sentimen', [InfoUserController::class, 'store']);
Route::get('/login', function () {
return view('dashboard');
})->name('sign-up');
Route::get('/logout', [SessionsController::class, 'destroy']);
Route::get('/data-sentimen', [InfoUserController::class, 'create']);
Route::post('/data-sentimen', [InfoUserController::class, 'store']);
});
Route::group(['middleware' => 'guest'], function () {
Route::get('/register', [RegisterController::class, 'create']);
Route::post('/register', [RegisterController::class, 'store']);
Route::get('/login', [SessionsController::class, 'create']);
Route::post('/session', [SessionsController::class, 'store']);
Route::get('/login/forgot-password', [ResetController::class, 'create']);
Route::post('/forgot-password', [ResetController::class, 'sendEmail']);
Route::get('/reset-password/{token}', [ResetController::class, 'resetPass'])->name('password.reset');
Route::post('/reset-password', [ChangePasswordController::class, 'changePassword'])->name('password.update');
Route::get('/register', [RegisterController::class, 'create']);
Route::post('/register', [RegisterController::class, 'store']);
Route::get('/login', [SessionsController::class, 'create']);
Route::post('/session', [SessionsController::class, 'store']);
Route::get('/login/forgot-password', [ResetController::class, 'create']);
Route::post('/forgot-password', [ResetController::class, 'sendEmail']);
Route::get('/reset-password/{token}', [ResetController::class, 'resetPass'])
->name('password.reset');
Route::post('/reset-password', [ChangePasswordController::class, 'changePassword'])
->name('password.update');
});
Route::get('/login', function () {
return view('session/login-session');
})->name('login');
Route::get('/wordcloud-data', [HomeController::class, 'wordcloudData']);

View File

@ -2774,6 +2774,78 @@ return array(
'League\\Config\\MutableConfigurationInterface' => $vendorDir . '/league/config/src/MutableConfigurationInterface.php',
'League\\Config\\ReadOnlyConfiguration' => $vendorDir . '/league/config/src/ReadOnlyConfiguration.php',
'League\\Config\\SchemaBuilderInterface' => $vendorDir . '/league/config/src/SchemaBuilderInterface.php',
'League\\Csv\\AbstractCsv' => $vendorDir . '/league/csv/src/AbstractCsv.php',
'League\\Csv\\Bom' => $vendorDir . '/league/csv/src/Bom.php',
'League\\Csv\\Buffer' => $vendorDir . '/league/csv/src/Buffer.php',
'League\\Csv\\ByteSequence' => $vendorDir . '/league/csv/src/ByteSequence.php',
'League\\Csv\\CallbackStreamFilter' => $vendorDir . '/league/csv/src/CallbackStreamFilter.php',
'League\\Csv\\CannotInsertRecord' => $vendorDir . '/league/csv/src/CannotInsertRecord.php',
'League\\Csv\\CharsetConverter' => $vendorDir . '/league/csv/src/CharsetConverter.php',
'League\\Csv\\ColumnConsistency' => $vendorDir . '/league/csv/src/ColumnConsistency.php',
'League\\Csv\\EncloseField' => $vendorDir . '/league/csv/src/EncloseField.php',
'League\\Csv\\EscapeFormula' => $vendorDir . '/league/csv/src/EscapeFormula.php',
'League\\Csv\\Exception' => $vendorDir . '/league/csv/src/Exception.php',
'League\\Csv\\FragmentFinder' => $vendorDir . '/league/csv/src/FragmentFinder.php',
'League\\Csv\\FragmentNotFound' => $vendorDir . '/league/csv/src/FragmentNotFound.php',
'League\\Csv\\HTMLConverter' => $vendorDir . '/league/csv/src/HTMLConverter.php',
'League\\Csv\\HttpHeaders' => $vendorDir . '/league/csv/src/HttpHeaders.php',
'League\\Csv\\Info' => $vendorDir . '/league/csv/src/Info.php',
'League\\Csv\\InvalidArgument' => $vendorDir . '/league/csv/src/InvalidArgument.php',
'League\\Csv\\JsonConverter' => $vendorDir . '/league/csv/src/JsonConverter.php',
'League\\Csv\\MapIterator' => $vendorDir . '/league/csv/src/MapIterator.php',
'League\\Csv\\Query\\Constraint\\Column' => $vendorDir . '/league/csv/src/Query/Constraint/Column.php',
'League\\Csv\\Query\\Constraint\\Comparison' => $vendorDir . '/league/csv/src/Query/Constraint/Comparison.php',
'League\\Csv\\Query\\Constraint\\Criteria' => $vendorDir . '/league/csv/src/Query/Constraint/Criteria.php',
'League\\Csv\\Query\\Constraint\\Offset' => $vendorDir . '/league/csv/src/Query/Constraint/Offset.php',
'League\\Csv\\Query\\Constraint\\TwoColumns' => $vendorDir . '/league/csv/src/Query/Constraint/TwoColumns.php',
'League\\Csv\\Query\\Limit' => $vendorDir . '/league/csv/src/Query/Limit.php',
'League\\Csv\\Query\\Ordering\\Column' => $vendorDir . '/league/csv/src/Query/Ordering/Column.php',
'League\\Csv\\Query\\Ordering\\MultiSort' => $vendorDir . '/league/csv/src/Query/Ordering/MultiSort.php',
'League\\Csv\\Query\\Predicate' => $vendorDir . '/league/csv/src/Query/Predicate.php',
'League\\Csv\\Query\\PredicateCombinator' => $vendorDir . '/league/csv/src/Query/PredicateCombinator.php',
'League\\Csv\\Query\\QueryException' => $vendorDir . '/league/csv/src/Query/QueryException.php',
'League\\Csv\\Query\\Row' => $vendorDir . '/league/csv/src/Query/Row.php',
'League\\Csv\\Query\\Sort' => $vendorDir . '/league/csv/src/Query/Sort.php',
'League\\Csv\\Query\\SortCombinator' => $vendorDir . '/league/csv/src/Query/SortCombinator.php',
'League\\Csv\\RFC4180Field' => $vendorDir . '/league/csv/src/RFC4180Field.php',
'League\\Csv\\RdbmsResult' => $vendorDir . '/league/csv/src/RdbmsResult.php',
'League\\Csv\\Reader' => $vendorDir . '/league/csv/src/Reader.php',
'League\\Csv\\ResultSet' => $vendorDir . '/league/csv/src/ResultSet.php',
'League\\Csv\\Serializer\\AfterMapping' => $vendorDir . '/league/csv/src/Serializer/AfterMapping.php',
'League\\Csv\\Serializer\\ArrayShape' => $vendorDir . '/league/csv/src/Serializer/ArrayShape.php',
'League\\Csv\\Serializer\\CallbackCasting' => $vendorDir . '/league/csv/src/Serializer/CallbackCasting.php',
'League\\Csv\\Serializer\\CastToArray' => $vendorDir . '/league/csv/src/Serializer/CastToArray.php',
'League\\Csv\\Serializer\\CastToBool' => $vendorDir . '/league/csv/src/Serializer/CastToBool.php',
'League\\Csv\\Serializer\\CastToDate' => $vendorDir . '/league/csv/src/Serializer/CastToDate.php',
'League\\Csv\\Serializer\\CastToEnum' => $vendorDir . '/league/csv/src/Serializer/CastToEnum.php',
'League\\Csv\\Serializer\\CastToFloat' => $vendorDir . '/league/csv/src/Serializer/CastToFloat.php',
'League\\Csv\\Serializer\\CastToInt' => $vendorDir . '/league/csv/src/Serializer/CastToInt.php',
'League\\Csv\\Serializer\\CastToString' => $vendorDir . '/league/csv/src/Serializer/CastToString.php',
'League\\Csv\\Serializer\\DenormalizationFailed' => $vendorDir . '/league/csv/src/Serializer/DenormalizationFailed.php',
'League\\Csv\\Serializer\\Denormalizer' => $vendorDir . '/league/csv/src/Serializer/Denormalizer.php',
'League\\Csv\\Serializer\\MapCell' => $vendorDir . '/league/csv/src/Serializer/MapCell.php',
'League\\Csv\\Serializer\\MapRecord' => $vendorDir . '/league/csv/src/Serializer/MapRecord.php',
'League\\Csv\\Serializer\\MappingFailed' => $vendorDir . '/league/csv/src/Serializer/MappingFailed.php',
'League\\Csv\\Serializer\\PropertySetter' => $vendorDir . '/league/csv/src/Serializer/PropertySetter.php',
'League\\Csv\\Serializer\\SerializationFailed' => $vendorDir . '/league/csv/src/Serializer/SerializationFailed.php',
'League\\Csv\\Serializer\\Type' => $vendorDir . '/league/csv/src/Serializer/Type.php',
'League\\Csv\\Serializer\\TypeCasting' => $vendorDir . '/league/csv/src/Serializer/TypeCasting.php',
'League\\Csv\\Serializer\\TypeCastingFailed' => $vendorDir . '/league/csv/src/Serializer/TypeCastingFailed.php',
'League\\Csv\\Serializer\\TypeCastingInfo' => $vendorDir . '/league/csv/src/Serializer/TypeCastingInfo.php',
'League\\Csv\\Serializer\\TypeCastingTargetType' => $vendorDir . '/league/csv/src/Serializer/TypeCastingTargetType.php',
'League\\Csv\\Statement' => $vendorDir . '/league/csv/src/Statement.php',
'League\\Csv\\Stream' => $vendorDir . '/league/csv/src/Stream.php',
'League\\Csv\\StreamFilter' => $vendorDir . '/league/csv/src/StreamFilter.php',
'League\\Csv\\SwapDelimiter' => $vendorDir . '/league/csv/src/SwapDelimiter.php',
'League\\Csv\\SyntaxError' => $vendorDir . '/league/csv/src/SyntaxError.php',
'League\\Csv\\TabularData' => $vendorDir . '/league/csv/src/TabularData.php',
'League\\Csv\\TabularDataReader' => $vendorDir . '/league/csv/src/TabularDataReader.php',
'League\\Csv\\TabularDataWriter' => $vendorDir . '/league/csv/src/TabularDataWriter.php',
'League\\Csv\\UnableToProcessCsv' => $vendorDir . '/league/csv/src/UnableToProcessCsv.php',
'League\\Csv\\UnavailableFeature' => $vendorDir . '/league/csv/src/UnavailableFeature.php',
'League\\Csv\\UnavailableStream' => $vendorDir . '/league/csv/src/UnavailableStream.php',
'League\\Csv\\Writer' => $vendorDir . '/league/csv/src/Writer.php',
'League\\Csv\\XMLConverter' => $vendorDir . '/league/csv/src/XMLConverter.php',
'League\\Flysystem\\CalculateChecksumFromStream' => $vendorDir . '/league/flysystem/src/CalculateChecksumFromStream.php',
'League\\Flysystem\\ChecksumAlgoIsNotSupported' => $vendorDir . '/league/flysystem/src/ChecksumAlgoIsNotSupported.php',
'League\\Flysystem\\ChecksumProvider' => $vendorDir . '/league/flysystem/src/ChecksumProvider.php',

View File

@ -35,6 +35,7 @@ return array(
'7f7ac2ddea9cc3fb4b2cc201d63dbc10' => $vendorDir . '/laravel/framework/src/Illuminate/Log/functions.php',
'493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php',
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
'9e4824c5afbdc1482b6025ce3d4dfde8' => $vendorDir . '/league/csv/src/functions_include.php',
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
'a1cfe24d14977df6878b9bf804af2d1c' => $vendorDir . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',

View File

@ -64,6 +64,7 @@ return array(
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\Local\\' => array($vendorDir . '/league/flysystem-local'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
'League\\Csv\\' => array($vendorDir . '/league/csv/src'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),

View File

@ -36,6 +36,7 @@ class ComposerStaticInit44af905ee6d3a491d075107e62a3ab1e
'7f7ac2ddea9cc3fb4b2cc201d63dbc10' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Log/functions.php',
'493c6aea52f6009bab023b26c21a386a' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/functions.php',
'58571171fd5812e6e447dce228f52f4d' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/helpers.php',
'9e4824c5afbdc1482b6025ce3d4dfde8' => __DIR__ . '/..' . '/league/csv/src/functions_include.php',
'c72349b1fe8d0deeedd3a52e8aa814d8' => __DIR__ . '/..' . '/mockery/mockery/library/helpers.php',
'ce9671a430e4846b44e1c68c7611f9f5' => __DIR__ . '/..' . '/mockery/mockery/library/Mockery.php',
'a1cfe24d14977df6878b9bf804af2d1c' => __DIR__ . '/..' . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',
@ -128,6 +129,7 @@ class ComposerStaticInit44af905ee6d3a491d075107e62a3ab1e
'League\\MimeTypeDetection\\' => 25,
'League\\Flysystem\\Local\\' => 23,
'League\\Flysystem\\' => 17,
'League\\Csv\\' => 11,
'League\\Config\\' => 14,
'League\\CommonMark\\' => 18,
'Laravel\\Tinker\\' => 15,
@ -421,6 +423,10 @@ class ComposerStaticInit44af905ee6d3a491d075107e62a3ab1e
array (
0 => __DIR__ . '/..' . '/league/flysystem/src',
),
'League\\Csv\\' =>
array (
0 => __DIR__ . '/..' . '/league/csv/src',
),
'League\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/league/config/src',
@ -3310,6 +3316,78 @@ class ComposerStaticInit44af905ee6d3a491d075107e62a3ab1e
'League\\Config\\MutableConfigurationInterface' => __DIR__ . '/..' . '/league/config/src/MutableConfigurationInterface.php',
'League\\Config\\ReadOnlyConfiguration' => __DIR__ . '/..' . '/league/config/src/ReadOnlyConfiguration.php',
'League\\Config\\SchemaBuilderInterface' => __DIR__ . '/..' . '/league/config/src/SchemaBuilderInterface.php',
'League\\Csv\\AbstractCsv' => __DIR__ . '/..' . '/league/csv/src/AbstractCsv.php',
'League\\Csv\\Bom' => __DIR__ . '/..' . '/league/csv/src/Bom.php',
'League\\Csv\\Buffer' => __DIR__ . '/..' . '/league/csv/src/Buffer.php',
'League\\Csv\\ByteSequence' => __DIR__ . '/..' . '/league/csv/src/ByteSequence.php',
'League\\Csv\\CallbackStreamFilter' => __DIR__ . '/..' . '/league/csv/src/CallbackStreamFilter.php',
'League\\Csv\\CannotInsertRecord' => __DIR__ . '/..' . '/league/csv/src/CannotInsertRecord.php',
'League\\Csv\\CharsetConverter' => __DIR__ . '/..' . '/league/csv/src/CharsetConverter.php',
'League\\Csv\\ColumnConsistency' => __DIR__ . '/..' . '/league/csv/src/ColumnConsistency.php',
'League\\Csv\\EncloseField' => __DIR__ . '/..' . '/league/csv/src/EncloseField.php',
'League\\Csv\\EscapeFormula' => __DIR__ . '/..' . '/league/csv/src/EscapeFormula.php',
'League\\Csv\\Exception' => __DIR__ . '/..' . '/league/csv/src/Exception.php',
'League\\Csv\\FragmentFinder' => __DIR__ . '/..' . '/league/csv/src/FragmentFinder.php',
'League\\Csv\\FragmentNotFound' => __DIR__ . '/..' . '/league/csv/src/FragmentNotFound.php',
'League\\Csv\\HTMLConverter' => __DIR__ . '/..' . '/league/csv/src/HTMLConverter.php',
'League\\Csv\\HttpHeaders' => __DIR__ . '/..' . '/league/csv/src/HttpHeaders.php',
'League\\Csv\\Info' => __DIR__ . '/..' . '/league/csv/src/Info.php',
'League\\Csv\\InvalidArgument' => __DIR__ . '/..' . '/league/csv/src/InvalidArgument.php',
'League\\Csv\\JsonConverter' => __DIR__ . '/..' . '/league/csv/src/JsonConverter.php',
'League\\Csv\\MapIterator' => __DIR__ . '/..' . '/league/csv/src/MapIterator.php',
'League\\Csv\\Query\\Constraint\\Column' => __DIR__ . '/..' . '/league/csv/src/Query/Constraint/Column.php',
'League\\Csv\\Query\\Constraint\\Comparison' => __DIR__ . '/..' . '/league/csv/src/Query/Constraint/Comparison.php',
'League\\Csv\\Query\\Constraint\\Criteria' => __DIR__ . '/..' . '/league/csv/src/Query/Constraint/Criteria.php',
'League\\Csv\\Query\\Constraint\\Offset' => __DIR__ . '/..' . '/league/csv/src/Query/Constraint/Offset.php',
'League\\Csv\\Query\\Constraint\\TwoColumns' => __DIR__ . '/..' . '/league/csv/src/Query/Constraint/TwoColumns.php',
'League\\Csv\\Query\\Limit' => __DIR__ . '/..' . '/league/csv/src/Query/Limit.php',
'League\\Csv\\Query\\Ordering\\Column' => __DIR__ . '/..' . '/league/csv/src/Query/Ordering/Column.php',
'League\\Csv\\Query\\Ordering\\MultiSort' => __DIR__ . '/..' . '/league/csv/src/Query/Ordering/MultiSort.php',
'League\\Csv\\Query\\Predicate' => __DIR__ . '/..' . '/league/csv/src/Query/Predicate.php',
'League\\Csv\\Query\\PredicateCombinator' => __DIR__ . '/..' . '/league/csv/src/Query/PredicateCombinator.php',
'League\\Csv\\Query\\QueryException' => __DIR__ . '/..' . '/league/csv/src/Query/QueryException.php',
'League\\Csv\\Query\\Row' => __DIR__ . '/..' . '/league/csv/src/Query/Row.php',
'League\\Csv\\Query\\Sort' => __DIR__ . '/..' . '/league/csv/src/Query/Sort.php',
'League\\Csv\\Query\\SortCombinator' => __DIR__ . '/..' . '/league/csv/src/Query/SortCombinator.php',
'League\\Csv\\RFC4180Field' => __DIR__ . '/..' . '/league/csv/src/RFC4180Field.php',
'League\\Csv\\RdbmsResult' => __DIR__ . '/..' . '/league/csv/src/RdbmsResult.php',
'League\\Csv\\Reader' => __DIR__ . '/..' . '/league/csv/src/Reader.php',
'League\\Csv\\ResultSet' => __DIR__ . '/..' . '/league/csv/src/ResultSet.php',
'League\\Csv\\Serializer\\AfterMapping' => __DIR__ . '/..' . '/league/csv/src/Serializer/AfterMapping.php',
'League\\Csv\\Serializer\\ArrayShape' => __DIR__ . '/..' . '/league/csv/src/Serializer/ArrayShape.php',
'League\\Csv\\Serializer\\CallbackCasting' => __DIR__ . '/..' . '/league/csv/src/Serializer/CallbackCasting.php',
'League\\Csv\\Serializer\\CastToArray' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToArray.php',
'League\\Csv\\Serializer\\CastToBool' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToBool.php',
'League\\Csv\\Serializer\\CastToDate' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToDate.php',
'League\\Csv\\Serializer\\CastToEnum' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToEnum.php',
'League\\Csv\\Serializer\\CastToFloat' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToFloat.php',
'League\\Csv\\Serializer\\CastToInt' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToInt.php',
'League\\Csv\\Serializer\\CastToString' => __DIR__ . '/..' . '/league/csv/src/Serializer/CastToString.php',
'League\\Csv\\Serializer\\DenormalizationFailed' => __DIR__ . '/..' . '/league/csv/src/Serializer/DenormalizationFailed.php',
'League\\Csv\\Serializer\\Denormalizer' => __DIR__ . '/..' . '/league/csv/src/Serializer/Denormalizer.php',
'League\\Csv\\Serializer\\MapCell' => __DIR__ . '/..' . '/league/csv/src/Serializer/MapCell.php',
'League\\Csv\\Serializer\\MapRecord' => __DIR__ . '/..' . '/league/csv/src/Serializer/MapRecord.php',
'League\\Csv\\Serializer\\MappingFailed' => __DIR__ . '/..' . '/league/csv/src/Serializer/MappingFailed.php',
'League\\Csv\\Serializer\\PropertySetter' => __DIR__ . '/..' . '/league/csv/src/Serializer/PropertySetter.php',
'League\\Csv\\Serializer\\SerializationFailed' => __DIR__ . '/..' . '/league/csv/src/Serializer/SerializationFailed.php',
'League\\Csv\\Serializer\\Type' => __DIR__ . '/..' . '/league/csv/src/Serializer/Type.php',
'League\\Csv\\Serializer\\TypeCasting' => __DIR__ . '/..' . '/league/csv/src/Serializer/TypeCasting.php',
'League\\Csv\\Serializer\\TypeCastingFailed' => __DIR__ . '/..' . '/league/csv/src/Serializer/TypeCastingFailed.php',
'League\\Csv\\Serializer\\TypeCastingInfo' => __DIR__ . '/..' . '/league/csv/src/Serializer/TypeCastingInfo.php',
'League\\Csv\\Serializer\\TypeCastingTargetType' => __DIR__ . '/..' . '/league/csv/src/Serializer/TypeCastingTargetType.php',
'League\\Csv\\Statement' => __DIR__ . '/..' . '/league/csv/src/Statement.php',
'League\\Csv\\Stream' => __DIR__ . '/..' . '/league/csv/src/Stream.php',
'League\\Csv\\StreamFilter' => __DIR__ . '/..' . '/league/csv/src/StreamFilter.php',
'League\\Csv\\SwapDelimiter' => __DIR__ . '/..' . '/league/csv/src/SwapDelimiter.php',
'League\\Csv\\SyntaxError' => __DIR__ . '/..' . '/league/csv/src/SyntaxError.php',
'League\\Csv\\TabularData' => __DIR__ . '/..' . '/league/csv/src/TabularData.php',
'League\\Csv\\TabularDataReader' => __DIR__ . '/..' . '/league/csv/src/TabularDataReader.php',
'League\\Csv\\TabularDataWriter' => __DIR__ . '/..' . '/league/csv/src/TabularDataWriter.php',
'League\\Csv\\UnableToProcessCsv' => __DIR__ . '/..' . '/league/csv/src/UnableToProcessCsv.php',
'League\\Csv\\UnavailableFeature' => __DIR__ . '/..' . '/league/csv/src/UnavailableFeature.php',
'League\\Csv\\UnavailableStream' => __DIR__ . '/..' . '/league/csv/src/UnavailableStream.php',
'League\\Csv\\Writer' => __DIR__ . '/..' . '/league/csv/src/Writer.php',
'League\\Csv\\XMLConverter' => __DIR__ . '/..' . '/league/csv/src/XMLConverter.php',
'League\\Flysystem\\CalculateChecksumFromStream' => __DIR__ . '/..' . '/league/flysystem/src/CalculateChecksumFromStream.php',
'League\\Flysystem\\ChecksumAlgoIsNotSupported' => __DIR__ . '/..' . '/league/flysystem/src/ChecksumAlgoIsNotSupported.php',
'League\\Flysystem\\ChecksumProvider' => __DIR__ . '/..' . '/league/flysystem/src/ChecksumProvider.php',

View File

@ -2022,6 +2022,100 @@
],
"install-path": "../league/config"
},
{
"name": "league/csv",
"version": "9.23.0",
"version_normalized": "9.23.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "774008ad8a634448e4f8e288905e070e8b317ff3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/774008ad8a634448e4f8e288905e070e8b317ff3",
"reference": "774008ad8a634448e4f8e288905e070e8b317ff3",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^8.1.2"
},
"require-dev": {
"ext-dom": "*",
"ext-xdebug": "*",
"friendsofphp/php-cs-fixer": "^3.69.0",
"phpbench/phpbench": "^1.4.0",
"phpstan/phpstan": "^1.12.18",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
"phpunit/phpunit": "^10.5.16 || ^11.5.7",
"symfony/var-dumper": "^6.4.8 || ^7.2.3"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters",
"ext-mysqli": "Requiered to use the package with the MySQLi extension",
"ext-pdo": "Required to use the package with the PDO extension",
"ext-pgsql": "Requiered to use the package with the PgSQL extension",
"ext-sqlite3": "Required to use the package with the SQLite3 extension"
},
"time": "2025-03-28T06:52:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"League\\Csv\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "CSV data manipulation made easy in PHP",
"homepage": "https://csv.thephpleague.com",
"keywords": [
"convert",
"csv",
"export",
"filter",
"import",
"read",
"transform",
"write"
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"install-path": "../league/csv"
},
{
"name": "league/flysystem",
"version": "3.29.1",

View File

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'laravel/laravel',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '8e91166adccbcaa340c47bd689164ad8decfe9b8',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b15a1f6a46af75b89beb3103e55c58524b4078c8',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -386,9 +386,9 @@
'dev_requirement' => false,
),
'laravel/laravel' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '8e91166adccbcaa340c47bd689164ad8decfe9b8',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b15a1f6a46af75b89beb3103e55c58524b4078c8',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -457,6 +457,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'league/csv' => array(
'pretty_version' => '9.23.0',
'version' => '9.23.0.0',
'reference' => '774008ad8a634448e4f8e288905e070e8b317ff3',
'type' => 'library',
'install_path' => __DIR__ . '/../league/csv',
'aliases' => array(),
'dev_requirement' => false,
),
'league/flysystem' => array(
'pretty_version' => '3.29.1',
'version' => '3.29.1.0',

20
vendor/league/csv/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

14
vendor/league/csv/autoload.php vendored Normal file
View File

@ -0,0 +1,14 @@
<?php
require __DIR__ . '/src/functions_include.php';
spl_autoload_register(static function (string $class): void {
if (!str_starts_with($class, 'League\Csv\\')) {
return;
}
$file = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, 11)).'.php';
if (is_readable($file)) {
require $file;
}
});

87
vendor/league/csv/composer.json vendored Normal file
View File

@ -0,0 +1,87 @@
{
"name": "league/csv",
"type": "library",
"description" : "CSV data manipulation made easy in PHP",
"keywords": ["csv", "import", "export", "read", "write", "filter", "convert", "transform"],
"license": "MIT",
"homepage" : "https://csv.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://github.com/nyamsprod/",
"role" : "Developer"
}
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php": "^8.1.2",
"ext-filter": "*"
},
"require-dev": {
"ext-dom": "*",
"ext-xdebug": "*",
"friendsofphp/php-cs-fixer": "^3.69.0",
"phpbench/phpbench": "^1.4.0",
"phpstan/phpstan": "^1.12.18",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
"phpunit/phpunit": "^10.5.16 || ^11.5.7",
"symfony/var-dumper": "^6.4.8 || ^7.2.3"
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": ["src/functions_include.php"]
},
"scripts": {
"benchmark": "phpbench run src --report=default",
"phpcs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -vvv --diff --dry-run --allow-risky=yes --ansi",
"phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
"phpstan": "phpstan analyse -c phpstan.neon --ansi --memory-limit=256M",
"phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text",
"phpunit:min": "phpunit --no-coverage",
"test": [
"@phpunit",
"@phpstan",
"@phpcs"
]
},
"scripts-descriptions": {
"benchmark": "Runs benchmarks on writing and reader CSV documents",
"phpcs": "Runs coding style test suite",
"phpstan": "Runs complete codebase static analysis",
"phpunit": "Runs unit and functional testing",
"test": "Runs full test suite"
},
"suggest": {
"ext-dom" : "Required to use the XMLConverter and the HTMLConverter classes",
"ext-iconv" : "Needed to ease transcoding CSV using iconv stream filters",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters",
"ext-pdo": "Required to use the package with the PDO extension",
"ext-sqlite3": "Required to use the package with the SQLite3 extension",
"ext-mysqli": "Requiered to use the package with the MySQLi extension",
"ext-pgsql": "Requiered to use the package with the PgSQL extension"
},
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"config": {
"sort-packages": true
}
}

622
vendor/league/csv/src/AbstractCsv.php vendored Normal file
View File

@ -0,0 +1,622 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use Deprecated;
use Generator;
use InvalidArgumentException;
use RuntimeException;
use SplFileObject;
use Stringable;
use Throwable;
use function filter_var;
use function get_class;
use function rawurlencode;
use function sprintf;
use function str_replace;
use function str_split;
use function strcspn;
use function strlen;
use const FILTER_FLAG_STRIP_HIGH;
use const FILTER_FLAG_STRIP_LOW;
use const FILTER_UNSAFE_RAW;
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
/**
* An abstract class to enable CSV document loading.
*/
abstract class AbstractCsv implements ByteSequence
{
protected const STREAM_FILTER_MODE = STREAM_FILTER_READ;
/** @var array<string, bool> collection of stream filters. */
protected array $stream_filters = [];
protected ?Bom $input_bom = null;
protected ?Bom $output_bom = null;
protected string $delimiter = ',';
protected string $enclosure = '"';
protected string $escape = '\\';
protected bool $is_input_bom_included = false;
/** @var array<Closure(array): array> collection of Closure to format the record before reading. */
protected array $formatters = [];
/**
* @final This method should not be overwritten in child classes
*/
protected function __construct(protected readonly SplFileObject|Stream $document)
{
[$this->delimiter, $this->enclosure, $this->escape] = $this->document->getCsvControl();
$this->resetProperties();
}
/**
* Reset dynamic object properties to improve performance.
*/
protected function resetProperties(): void
{
}
/**
* @throws UnavailableStream
*/
public function __clone()
{
throw UnavailableStream::dueToForbiddenCloning(static::class);
}
/**
* Returns a new instance from a SplFileObject.
*/
public static function createFromFileObject(SplFileObject $file): static
{
return new static($file);
}
/**
* Returns a new instance from a PHP resource stream.
*
* @param resource $stream
*/
public static function createFromStream($stream): static
{
return new static(Stream::createFromResource($stream));
}
/**
* Returns a new instance from a string.
*/
public static function createFromString(Stringable|string $content = ''): static
{
return new static(Stream::createFromString((string) $content));
}
/**
* Returns a new instance from a file path.
*
* @param resource|null $context the resource context
*
* @throws UnavailableStream
*/
public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): static
{
return new static(Stream::createFromPath($path, $open_mode, $context));
}
/**
* Returns the current field delimiter.
*/
public function getDelimiter(): string
{
return $this->delimiter;
}
/**
* Returns the current field enclosure.
*/
public function getEnclosure(): string
{
return $this->enclosure;
}
/**
* Returns the pathname of the underlying document.
*/
public function getPathname(): string
{
return $this->document->getPathname();
}
/**
* Returns the current field escape character.
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* Returns the BOM sequence in use on Output methods.
*/
public function getOutputBOM(): string
{
return $this->output_bom?->value ?? '';
}
/**
* Returns the BOM sequence of the given CSV.
*/
public function getInputBOM(): string
{
if (null === $this->input_bom) {
$this->document->setFlags(SplFileObject::READ_CSV);
$this->input_bom = Bom::tryFromSequence($this->document);
}
return $this->input_bom?->value ?? '';
}
/**
* Tells whether the stream filter read capabilities can be used.
*/
public function supportsStreamFilterOnRead(): bool
{
if (!$this->document instanceof Stream) {
return false;
}
$mode = $this->document->getMode();
return strcspn($mode, 'r+') !== strlen($mode);
}
/**
* Tells whether the stream filter write capabilities can be used.
*/
public function supportsStreamFilterOnWrite(): bool
{
if (!$this->document instanceof Stream) {
return false;
}
$mode = $this->document->getMode();
return strcspn($mode, 'wae+') !== strlen($mode);
}
/**
* Tells whether the specified stream filter is attached to the current stream.
*/
public function hasStreamFilter(string $filtername): bool
{
return $this->stream_filters[$filtername] ?? false;
}
/**
* Tells whether the BOM can be stripped if presents.
*/
public function isInputBOMIncluded(): bool
{
return $this->is_input_bom_included;
}
/**
* Returns the CSV document as a Generator of string chunk.
*
* @throws Exception if the number of bytes is less than 1
*/
public function chunk(int $length): Generator
{
if ($length < 1) {
throw InvalidArgument::dueToInvalidChunkSize($length, __METHOD__);
}
$this->document->rewind();
$this->document->setFlags(0);
if (-1 === $this->document->fseek(strlen($this->getInputBOM()))) {
throw new RuntimeException('Unable to seek the document.');
}
yield from str_split($this->getOutputBOM().$this->document->fread($length), $length);
while ($this->document->valid()) {
yield $this->document->fread($length);
}
}
/**
* Retrieves the CSV content.
*
* @throws Exception If the string representation can not be returned
*/
public function toString(): string
{
$raw = '';
foreach ($this->chunk(8192) as $chunk) {
$raw .= $chunk;
}
return $raw;
}
/**
* Outputs all data on the CSV file.
*
* Returns the number of characters read from the handle and passed through to the output.
*
* @throws InvalidArgumentException|Exception
*/
public function download(?string $filename = null): int
{
if (null !== $filename) {
HttpHeaders::forFileDownload($filename, 'text/csv');
}
$this->document->rewind();
$this->document->setFlags(0);
if (!$this->is_input_bom_included && -1 === $this->document->fseek(strlen($this->getInputBOM()))) {
throw new RuntimeException('Unable to seek the document.');
}
$stream = Stream::createFromString($this->getOutputBOM());
$stream->rewind();
$res1 = $stream->fpassthru();
if (false === $res1) {
throw new RuntimeException('Unable to output the document.');
}
$res2 = $this->document->fpassthru();
if (false === $res2) {
throw new RuntimeException('Unable to output the document.');
}
return $res1 + $res2;
}
/**
* Sets the field delimiter.
*
* @throws InvalidArgument If the Csv control character is not one character only.
*/
public function setDelimiter(string $delimiter): static
{
if ($delimiter === $this->delimiter) {
return $this;
}
1 === strlen($delimiter) || throw InvalidArgument::dueToInvalidDelimiterCharacter($delimiter, __METHOD__);
$this->delimiter = $delimiter;
$this->resetProperties();
return $this;
}
/**
* Sets the field enclosure.
*
* @throws InvalidArgument If the Csv control character is not one character only.
*/
public function setEnclosure(string $enclosure): static
{
if ($enclosure === $this->enclosure) {
return $this;
}
1 === strlen($enclosure) || throw InvalidArgument::dueToInvalidEnclosureCharacter($enclosure, __METHOD__);
$this->enclosure = $enclosure;
$this->resetProperties();
return $this;
}
/**
* Sets the field escape character.
*
* @throws InvalidArgument If the Csv control character is not one character only.
*/
public function setEscape(string $escape): static
{
if ($escape === $this->escape) {
return $this;
}
if ('' !== $escape && 1 !== strlen($escape)) {
throw InvalidArgument::dueToInvalidEscapeCharacter($escape, __METHOD__);
}
$this->escape = $escape;
$this->resetProperties();
return $this;
}
/**
* Adds a record formatter.
*
* @param callable(array): array $formatter
*/
public function addFormatter(callable $formatter): static
{
$this->formatters[] = !$formatter instanceof Closure ? $formatter(...) : $formatter;
return $this;
}
/**
* Enables BOM Stripping.
*/
public function skipInputBOM(): static
{
$this->is_input_bom_included = false;
return $this;
}
/**
* Disables skipping Input BOM.
*/
public function includeInputBOM(): static
{
$this->is_input_bom_included = true;
return $this;
}
/**
* Sets the BOM sequence to prepend the CSV on output.
*
* @throws InvalidArgument if the given non-empty string is not a valid BOM sequence
*/
public function setOutputBOM(Bom|string|null $str): static
{
try {
$this->output_bom = match (true) {
$str instanceof Bom => $str,
null === $str,
'' === $str => null,
default => Bom::fromSequence($str),
};
return $this;
} catch (Throwable $exception) {
throw InvalidArgument::dueToInvalidBOMCharacter(__METHOD__, $exception);
}
}
/**
* Append a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
public function appendStreamFilterOnRead(string $filtername, mixed $params = null): static
{
$this->document instanceof Stream || throw UnavailableFeature::dueToUnsupportedStreamFilterApi(get_class($this->document));
$this->document->appendFilter($filtername, STREAM_FILTER_READ, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
/**
* Append a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
public function appendStreamFilterOnWrite(string $filtername, mixed $params = null): static
{
$this->document instanceof Stream || throw UnavailableFeature::dueToUnsupportedStreamFilterApi(get_class($this->document));
$this->document->appendFilter($filtername, STREAM_FILTER_WRITE, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
/**
* Prepend a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
public function prependStreamFilterOnWrite(string $filtername, mixed $params = null): static
{
$this->document instanceof Stream || throw UnavailableFeature::dueToUnsupportedStreamFilterApi(get_class($this->document));
$this->document->prependFilter($filtername, STREAM_FILTER_READ, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
/**
* Prepend a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
public function prependStreamFilterOnRead(string $filtername, mixed $params = null): static
{
$this->document instanceof Stream || throw UnavailableFeature::dueToUnsupportedStreamFilterApi(get_class($this->document));
$this->document->prependFilter($filtername, STREAM_FILTER_READ, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see AbstractCsv::supportsStreamFilterOnRead
* @see AbstractCsv::supportsStreamFilterOnWrite
* @codeCoverageIgnore
*
* Returns the stream filter mode.
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::supportsStreamFilterOnRead() or League\Csv\AbstractCsv::supportsStreamFilterOnWrite() instead', since:'league/csv:9.7.0')]
public function getStreamFilterMode(): int
{
return static::STREAM_FILTER_MODE;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see AbstractCsv::supportsStreamFilterOnRead
* @see AbstractCsv::supportsStreamFilterOnWrite
* @codeCoverageIgnore
*
* Tells whether the stream filter capabilities can be used.
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::supportsStreamFilterOnRead() or League\Csv\AbstractCsv::supportsStreamFilterOnWrite() instead', since:'league/csv:9.7.0')]
public function supportsStreamFilter(): bool
{
return $this->document instanceof Stream;
}
/**
* Retrieves the CSV content.
*
* DEPRECATION WARNING! This method will be removed in the next major point release
*
* @deprecated since version 9.7.0
* @see AbstractCsv::toString
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::toString() instead', since:'league/csv:9.7.0')]
public function getContent(): string
{
return $this->toString();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.1.0
* @see AbstractCsv::toString
* @codeCoverageIgnore
*
* Retrieves the CSV content
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::toString() instead', since:'league/csv:9.1.0')]
public function __toString(): string
{
return $this->toString();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws Exception if the submitted header is invalid according to RFC 6266
*
* @see HttpHeaders::forFileDownload()
* @codeCoverageIgnore
*
* Send the CSV headers.
*
* Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
*
* @deprecated since version 9.17.0
* @see https://tools.ietf.org/html/rfc6266#section-4.3
*/
#[Deprecated(message:'the method no longer affect the outcome of the class, use League\Csv\HttpHeaders::forFileDownload instead', since:'league/csv:9.17.0')]
protected function sendHeaders(string $filename): void
{
if (strlen($filename) !== strcspn($filename, '\\/')) {
throw InvalidArgument::dueToInvalidHeaderFilename($filename);
}
$flag = FILTER_FLAG_STRIP_LOW;
if (1 === preg_match('/[^\x20-\x7E]/', $filename)) {
$flag |= FILTER_FLAG_STRIP_HIGH;
}
/** @var string $filtered_name */
$filtered_name = filter_var($filename, FILTER_UNSAFE_RAW, $flag);
$filename_fallback = str_replace('%', '', $filtered_name);
$disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filename_fallback));
if ($filename !== $filename_fallback) {
$disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
}
header('Content-Type: text/csv');
header('Content-Transfer-Encoding: binary');
header('Content-Description: File Transfer');
header('Content-Disposition: '.$disposition);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @codeCoverageIgnore
* @deprecated since version 9.18.0
* @see AbstractCsv::download()
*
* Outputs all data on the CSV file.
*
* Returns the number of characters read from the handle and passed through to the output.
*
* @throws Exception
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::download() instead', since:'league/csv:9.18.0')]
public function output(?string $filename = null): int
{
try {
return $this->download($filename);
} catch (InvalidArgumentException $exception) {
throw new InvalidArgument($exception->getMessage());
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
* @codeCoverageIgnore
* @deprecated since version 9.22.0
* @see AbstractCsv::appendStreamFilterOnRead()
* @see AbstractCsv::appendStreamFilterOnWrite()
*
* Append a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
#[Deprecated(message:'use League\Csv\AbstractCsv::appendStreamFilterOnRead() or League\Csv\AbstractCsv::prependStreamFilterOnRead() instead', since:'league/csv:9.18.0')]
public function addStreamFilter(string $filtername, ?array $params = null): static
{
if (STREAM_FILTER_READ === static::STREAM_FILTER_MODE) {
return $this->appendStreamFilterOnRead($filtername, $params);
}
return $this->appendStreamFilterOnWrite($filtername, $params);
}
}

145
vendor/league/csv/src/Bom.php vendored Normal file
View File

@ -0,0 +1,145 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use SplFileObject;
use Stringable;
use Throwable;
use ValueError;
enum Bom: string
{
case Utf32Le = "\xFF\xFE\x00\x00";
case Utf32Be = "\x00\x00\xFE\xFF";
case Utf16Be = "\xFE\xFF";
case Utf16Le = "\xFF\xFE";
case Utf8 = "\xEF\xBB\xBF";
public static function fromSequence(mixed $sequence): self
{
return self::tryFromSequence($sequence)
?? throw new ValueError('No BOM sequence could be found on the given sequence.');
}
public static function tryFromSequence(mixed $sequence): ?self
{
$str = match (true) {
$sequence instanceof SplFileObject,
$sequence instanceof Stream => self::getContents($sequence, 4, 0),
is_resource($sequence) => stream_get_contents($sequence, 4, 0),
$sequence instanceof AbstractCsv => $sequence->getInputBOM(),
$sequence instanceof Stringable,
is_string($sequence) => substr((string) $sequence, 0, 4),
default => $sequence,
};
if (!is_string($str) || '' === rtrim($str)) {
return null;
}
foreach (self::cases() as $bom) {
if (str_starts_with($str, $bom->value)) {
return $bom;
}
}
return null;
}
private static function getContents(Stream|SplFileObject $sequence, int $length, int $offset): ?string
{
$position = $sequence->ftell();
if (false === $position) {
return null;
}
try {
$sequence->fseek($offset);
$str = $sequence->fread($length);
$sequence->fseek($position);
if (false === $str) {
return null;
}
return $str;
} catch (Throwable) {
return null;
}
}
public static function fromEncoding(string $name): self
{
return self::tryFromEncoding($name)
?? throw new ValueError('Unknown or unsupported BOM name `'.$name.'`.');
}
/**
* @see https://unicode.org/faq/utf_bom.html#gen7
*/
public static function tryFromEncoding(string $name): ?self
{
return match (strtoupper(str_replace(['_', '-'], '', $name))) {
'UTF8' => self::Utf8,
'UTF16',
'UTF16BE' => self::Utf16Be,
'UTF16LE' => self::Utf16Le,
'UTF32',
'UTF32BE' => self::Utf32Be,
'UTF32LE' => self::Utf32Le,
default => null,
};
}
public function length(): int
{
return strlen($this->value);
}
public function encoding(): string
{
return match ($this) {
self::Utf16Le => 'UTF-16LE',
self::Utf16Be => 'UTF-16BE',
self::Utf32Le => 'UTF-32LE',
self::Utf32Be => 'UTF-32BE',
self::Utf8 => 'UTF-8',
};
}
public function isUtf8(): bool
{
return match ($this) {
self::Utf8 => true,
default => false,
};
}
public function isUtf16(): bool
{
return match ($this) {
self::Utf16Le,
self::Utf16Be => true,
default => false,
};
}
public function isUtf32(): bool
{
return match ($this) {
self::Utf32Le,
self::Utf32Be => true,
default => false,
};
}
}

487
vendor/league/csv/src/Buffer.php vendored Normal file
View File

@ -0,0 +1,487 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use CallbackFilterIterator;
use Closure;
use Iterator;
use League\Csv\Query\Constraint\Criteria;
use League\Csv\Query\Predicate;
use League\Csv\Serializer\Denormalizer;
use League\Csv\Serializer\MappingFailed;
use League\Csv\Serializer\TypeCastingFailed;
use mysqli_result;
use PDOStatement;
use PgSql\Result;
use ReflectionException;
use RuntimeException;
use SQLite3Result;
use function array_combine;
use function array_diff;
use function array_fill_keys;
use function array_filter;
use function array_is_list;
use function array_key_exists;
use function array_key_first;
use function array_key_last;
use function array_keys;
use function array_map;
use function array_push;
use function array_reduce;
use function array_unique;
use function array_values;
use function count;
use function in_array;
use function is_int;
use function sort;
use const ARRAY_FILTER_USE_KEY;
final class Buffer implements TabularData
{
public const INCLUDE_HEADER = 1;
public const EXCLUDE_HEADER = 2;
/** @var list<string>|array{} */
private readonly array $header;
/** @var list<string>|array{} */
private readonly array $sortedHeader;
/** @var array<string, null> */
private readonly array $nullRecord;
/** @var array<int, list<mixed>> */
private array $rows = [];
/** @var array<Closure(array): bool> callable collection to validate the record before insertion. */
private array $validators = [];
/** @var array<Closure(array): array> collection of Closure to format the record before reading. */
private array $formatters = [];
/**
* @param list<string>|array{} $header
*
* @throws SyntaxError
*/
public function __construct(array $header = [])
{
$this->header = match (true) {
!array_is_list($header) => throw new SyntaxError('The header must be a list of unique column names.'),
$header !== array_filter($header, is_string(...)) => throw SyntaxError::dueToInvalidHeaderColumnNames(),
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
default => $header,
};
sort($header);
$this->sortedHeader = $header;
$this->nullRecord = array_fill_keys($this->header, null);
}
/**
* Returns a new instance from a tabular data implementing object.
*
* @throws RuntimeException|SyntaxError If the column names can not be found
*/
public static function from(PDOStatement|Result|mysqli_result|SQLite3Result|TabularData $dataStorage, int $options = self::INCLUDE_HEADER): self
{
/** @var Iterator<int, array> $rows */
$rows = $dataStorage instanceof TabularData ? $dataStorage->getRecords() : RdbmsResult::rows($dataStorage);
$instance = new self(match (true) {
self::EXCLUDE_HEADER === $options => [],
$dataStorage instanceof TabularData => $dataStorage->getHeader(),
default => RdbmsResult::columnNames($dataStorage),
});
/**
* @var int $offset
* @var list<mixed> $row
*/
foreach (new MapIterator($rows, fn (array $record): array => array_values($record)) as $offset => $row) {
$instance->rows[$offset] = $row;
}
return $instance;
}
/**
* @throws CannotInsertRecord
* @throws Exception
*/
public function to(TabularDataWriter $dataStorage, int $options = self::INCLUDE_HEADER): int
{
$bytes = 0;
$header = $this->getHeader();
if (self::INCLUDE_HEADER === $options && [] !== $header) {
$bytes += $dataStorage->insertOne($header);
}
return $bytes + $dataStorage->insertAll($this->getRecords());
}
public function isEmpty(): bool
{
return [] === $this->rows;
}
public function hasHeader(): bool
{
return [] !== $this->header;
}
public function recordCount(): int
{
return count($this->rows);
}
/**
* @return list<string>|array{}
*/
public function getHeader(): array
{
return $this->header;
}
/**
* @throws SyntaxError
*
* @return Iterator<int, array>
*/
public function getRecords(array $header = []): Iterator
{
$header = match (true) {
[] === $header => $this->header,
$header !== array_filter($header, is_int(...), ARRAY_FILTER_USE_KEY) => throw new SyntaxError('The header must be a list of unique column names.'),
$header !== array_filter($header, is_string(...)) => throw SyntaxError::dueToInvalidHeaderColumnNames(),
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
default => $header,
};
return MapIterator::fromIterable($this->rows, fn (array $row): array => $this->rowToRecord($row, $header));
}
/**
* @template T of object
* @param class-string<T> $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*
* @return iterator<T>
*/
public function getRecordsAsObject(string $className, array $header = []): Iterator
{
return Denormalizer::assignAll($className, $this->getRecords($header), [] === $header ? $this->header : $header);
}
/**
* Run a map over each container members.
*
* @template TMap
*
* @param callable(array, int): TMap $callback
*
* @throws SyntaxError
*
* @return Iterator<TMap>
*/
public function map(callable $callback): Iterator
{
return MapIterator::fromIterable($this->getRecords(), $callback);
}
/**
* @param non-negative-int $nth
*
* @throws InvalidArgument
*/
public function nth(int $nth): array
{
if ([] === ($row = $this->nthRow($nth, __METHOD__))) {
return [];
}
return $this->rowToRecord($row, $this->header);
}
/**
* @template T of object
*
* @param non-negative-int $nth
* @param class-string<T> $className
* @param array<string> $header
*
* @throws InvalidArgument
* @throws ReflectionException
*/
public function nthAsObject(int $nth, string $className, array $header = []): ?object
{
if ([] === ($row = $this->nthRow($nth, __METHOD__))) {
return null;
}
return Denormalizer::assign($className, $this->rowToRecord($row, [] !== $header ? $header : $this->header));
}
public function firstOffset(): ?int
{
return array_key_first($this->rows);
}
public function first(): array
{
return null === ($offset = $this->firstOffset()) ? [] : $this->rowToRecord($this->rows[$offset], $this->header);
}
/**
* @param class-string $className
* @param array<string> $header
*
* @throws ReflectionException
*/
public function firstAsObject(string $className, array $header = []): ?object
{
if ([] === ($row = $this->rows[$this->firstOffset()] ?? [])) {
return null;
}
return Denormalizer::assign($className, $this->rowToRecord($row, [] !== $header ? $header : $this->header));
}
public function lastOffset(): ?int
{
return array_key_last($this->rows);
}
public function last(): array
{
return null === ($offset = $this->lastOffset()) ? [] : $this->rowToRecord($this->rows[$offset], $this->header);
}
/**
* @param class-string $className
* @param array<string> $header
*
* @throws ReflectionException
*/
public function lastAsObject(string $className, array $header = []): ?object
{
if ([] === ($row = $this->rows[$this->lastOffset()] ?? [])) {
return null;
}
return Denormalizer::assign($className, $this->rowToRecord($row, [] !== $header ? $header : $this->header));
}
/**
* @throws InvalidArgument
*/
private function nthRow(int $nth, string $method): array
{
-1 < $nth || throw InvalidArgument::dueToInvalidRecordOffset($nth, $method);
if (null === ($first = $this->firstOffset())) {
return [];
}
$offset = $first + $nth;
if (!array_key_exists($offset, $this->rows)) {
return [];
}
return $this->rows[$nth + $first];
}
public function fetchColumn(int|string $index = 0): Iterator
{
if (is_int($index)) {
$index > -1 || throw InvalidArgument::dueToInvalidColumnIndex($index, 'offset', __METHOD__);
[] === $this->header || array_key_exists($index, $this->header) || throw InvalidArgument::dueToInvalidColumnIndex($index, 'name', __METHOD__);
$iterator = new MapIterator($this->getRecords(), fn (array $row) => array_values($row));
$iterator = new CallbackFilterIterator($iterator, fn (array $row) => array_key_exists($index, $row));
return new MapIterator($iterator, fn (array $row) => $row[$index]);
}
[] !== $this->header || throw InvalidArgument::dueToInvalidColumnIndex($index, 'name', __METHOD__);
in_array($index, $this->header, true) || throw InvalidArgument::dueToInvalidColumnIndex($index, 'name', __METHOD__);
$iterator = new CallbackFilterIterator($this->getRecords(), fn (array $row) => array_key_exists($index, $row));
return new MapIterator($iterator, fn (array $row) => $row[$index]);
}
/**
* Adds a record validator.
*
* @param callable(array): bool $validator
*/
public function addValidator(callable $validator, string $name): self
{
$this->validators[$name] = !$validator instanceof Closure ? $validator(...) : $validator;
return $this;
}
/**
* Adds a record formatter.
*
* @param callable(array): array $formatter
*/
public function addFormatter(callable $formatter): self
{
$this->formatters[] = !$formatter instanceof Closure ? $formatter(...) : $formatter;
return $this;
}
/**
* @throws CannotInsertRecord
*/
public function insert(array ...$records): int
{
[] !== $records || throw CannotInsertRecord::triggerOnValidation('@buffer_record_validation_on_insert', $records);
array_push($this->rows, ...array_map($this->formatInsertRecord(...), $records));
return count($records);
}
/**
* @throws CannotInsertRecord
* @throws SyntaxError
*/
public function update(Predicate|Closure|callable $where, array $record): int
{
$record = $this->filterUpdateRecord($record);
$updateRecord = function (array $row) use ($record): array {
foreach ($record as $index => $value) {
$row[$index] = $value;
}
return $this->validateRecord($row);
};
$affectedRecords = 0;
/** @var Iterator<int, array> $iterator */
$iterator = new MapIterator(new CallbackFilterIterator($this->getRecords(), $this->filterPredicate($where)), $updateRecord);
foreach ($iterator as $offset => $row) {
$this->rows[$offset] = $row;
$affectedRecords++;
}
return $affectedRecords;
}
/**
* @throws SyntaxError
*/
public function delete(Predicate|Closure|callable $where): int
{
$affectedRecords = 0;
foreach (new CallbackFilterIterator($this->getRecords(), $this->filterPredicate($where)) as $offset => $row) {
unset($this->rows[$offset]);
$affectedRecords++;
}
return $affectedRecords;
}
public function truncate(): void
{
$this->rows = [];
}
/**
* @throws CannotInsertRecord
*/
private function formatInsertRecord(array $record): array
{
$this->filterInsertRecord($record) || throw CannotInsertRecord::triggerOnValidation('@buffer_record_validation_on_insert', $record);
return $this->validateRecord(match (true) {
[] === $this->header => !array_is_list($record) ? array_values($record) : $record,
array_is_list($record) => array_combine($this->header, $record),
default => [...$this->nullRecord, ...$record],
});
}
private function filterInsertRecord(array $record): bool
{
if ([] === $this->header) {
return true;
}
if (array_is_list($record)) {
return count($record) === count($this->header);
}
$keys = array_keys($record);
sort($keys);
return $keys === $this->sortedHeader;
}
/**
* @throws CannotInsertRecord
*/
private function filterUpdateRecord(array $record): array
{
[] !== $record || throw CannotInsertRecord::triggerOnValidation('@buffer_record_validation_on_update', $record);
if (array_is_list($record)) {
return $this->rowToRecord($record, $this->header);
}
$keys = array_keys($record);
return match (true) {
$keys === array_filter($keys, is_int(...)) => $record,
$keys !== array_filter($keys, is_string(...)),
[] !== array_diff($keys, $this->header) => throw CannotInsertRecord::triggerOnValidation('@buffer_record_validation_on_update', $record),
default => $record,
};
}
/**
* Validates a record.
*
* @throws CannotInsertRecord If the validation failed
*/
private function validateRecord(array $record): array
{
$record = array_reduce($this->formatters, fn (array $record, callable $formatter): array => $formatter($record), $record);
foreach ($this->validators as $name => $validator) {
true === $validator($record) || throw CannotInsertRecord::triggerOnValidation($name, $record);
}
return !array_is_list($record) ? array_values($record) : $record;
}
private function filterPredicate(Predicate|Closure|callable $predicate): Predicate
{
return !$predicate instanceof Predicate ? Criteria::all($predicate) : $predicate;
}
private function rowToRecord(array $row, array $header): array
{
if ([] === $header) {
return $row;
}
$record = [];
foreach ($header as $offset => $headerName) {
$record[$headerName] = $row[$offset] ?? null;
}
return $record;
}
}

34
vendor/league/csv/src/ByteSequence.php vendored Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv;
use Deprecated;
/**
* Defines constants for common BOM sequences.
*
* @deprecated since version 9.16.0
* @see Bom
*/
interface ByteSequence
{
#[Deprecated(message: 'use League\Csv\Bom:Utf8 instead', since: 'league/csv:9.16.0')]
public const BOM_UTF8 = "\xEF\xBB\xBF";
#[Deprecated(message: 'use League\Csv\Bom:Utf16be instead', since: 'league/csv:9.16.0')]
public const BOM_UTF16_BE = "\xFE\xFF";
#[Deprecated(message: 'use League\Csv\Bom:Utf16Le instead', since: 'league/csv:9.16.0')]
public const BOM_UTF16_LE = "\xFF\xFE";
#[Deprecated(message: 'use League\Csv\Bom:Utf32Be instead', since: 'league/csv:9.16.0')]
public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
#[Deprecated(message: 'use League\Csv\Bom:Utf32Le instead', since: 'league/csv:9.16.0')]
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
}

View File

@ -0,0 +1,161 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use LogicException;
use OutOfBoundsException;
use php_user_filter;
use ReflectionException;
use ReflectionFunction;
use RuntimeException;
use Throwable;
use ValueError;
use function array_keys;
use function in_array;
use function restore_error_handler;
use function set_error_handler;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_bucket_new;
use function stream_filter_register;
use function stream_get_filters;
use const PSFS_ERR_FATAL;
use const PSFS_FEED_ME;
use const PSFS_PASS_ON;
final class CallbackStreamFilter extends php_user_filter
{
/** @var array<string, Closure(string, mixed): string> */
private static array $filters = [];
/** @var ?Closure(string, mixed): string */
private ?Closure $callback;
public function onCreate(): bool
{
$this->callback = self::$filters[$this->filtername] ?? null;
return $this->callback instanceof Closure;
}
public function onClose(): void
{
$this->callback = null;
}
public function filter($in, $out, &$consumed, bool $closing): int
{
$data = '';
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$data .= $bucket->data;
$consumed += $bucket->datalen;
}
if (null === $this->callback) {
return PSFS_FEED_ME;
}
try {
$data = ($this->callback)($data, $this->params);
} catch (Throwable $exception) {
$this->onClose();
trigger_error('An error occurred while executing the stream filter `'.$this->filtername.'`: '.$exception->getMessage(), E_USER_WARNING);
return PSFS_ERR_FATAL;
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
stream_bucket_append($out, stream_bucket_new($this->stream, $data));
restore_error_handler();
return PSFS_PASS_ON;
}
/**
* Static method to register the class as a stream filter.
*
* @param callable(string, mixed): string $callback
*/
public static function register(string $filtername, callable $callback): void
{
if (isset(self::$filters[$filtername]) || in_array($filtername, stream_get_filters(), true)) {
throw new LogicException('The stream filter "'.$filtername.'" is already registered.');
}
$callback = self::normalizeCallback($callback);
if (!stream_filter_register($filtername, self::class)) {
throw new RuntimeException('The stream filter "'.$filtername.'" could not be registered.');
}
self::$filters[$filtername] = $callback;
}
/**
* @param callable(string, mixed): string $callback
*
* @throws ReflectionException|ValueError
*
* @return Closure(string, mixed): string
*/
private static function normalizeCallback(callable $callback): Closure
{
if (!$callback instanceof Closure) {
$callback = $callback(...);
}
$reflection = new ReflectionFunction($callback);
if (!$reflection->isInternal()) {
return $callback;
}
if (1 !== $reflection->getNumberOfParameters()) {
throw new ValueError('The PHP function "'.$reflection->getName().'" can not be used directly; wrap it in a callback.');
}
return fn (string $bucket, mixed $params): string => $callback($bucket);
}
/**
* Tells whether a callback with the given name is already registered or not.
*/
public static function isRegistered(string $filtername): bool
{
return isset(self::$filters[$filtername]);
}
/**
* Returns the list of registered filters.
*
* @return array<string>
*/
public static function registeredFilternames(): array
{
return array_keys(self::$filters);
}
/**
* Returns the closure attached to the filtername.
*
* @throws OutOfBoundsException if no callback is attached to the filter name
*
* @return Closure(string, mixed): string
*/
public static function callback(string $filtername): Closure
{
return self::$filters[$filtername] ?? throw new OutOfBoundsException('No callback is attached to the stream filter "'.$filtername.'".');
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
/**
* Thrown when a data is not added to the Csv Document.
*/
class CannotInsertRecord extends Exception
{
/** The record submitted for insertion. */
protected array $record = [];
/** Validator which did not validate the data. */
protected string $name = '';
/**
* Creates an Exception from a record insertion into a stream.
*/
public static function triggerOnInsertion(array $record): self
{
$exception = new self('Unable to write record to the CSV document');
$exception->record = $record;
return $exception;
}
/**
* Creates an Exception from a Record Validation.
*/
public static function triggerOnValidation(string $name, array $record): self
{
$exception = new self('Record validation failed');
$exception->name = $name;
$exception->record = $record;
return $exception;
}
/**
* Returns the validator name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns the invalid data submitted.
*/
public function getRecord(): array
{
return $this->record;
}
}

View File

@ -0,0 +1,418 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use OutOfRangeException;
use php_user_filter;
use RuntimeException;
use Throwable;
use TypeError;
use function array_map;
use function array_reduce;
use function get_resource_type;
use function gettype;
use function in_array;
use function is_numeric;
use function is_resource;
use function mb_convert_encoding;
use function mb_list_encodings;
use function preg_match;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_bucket_new;
use function stream_filter_append;
use function stream_filter_prepend;
use function stream_filter_register;
use function stream_get_filters;
use function strtolower;
use function substr;
use const PSFS_ERR_FATAL;
use const PSFS_FEED_ME;
use const PSFS_PASS_ON;
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
/**
* Converts resource stream or tabular data content charset.
*/
class CharsetConverter extends php_user_filter
{
public const FILTERNAME = 'convert.league.csv';
public const BOM_SEQUENCE = 'bom_sequence';
public const SKIP_BOM_SEQUENCE = 'skip_bom_sequence';
protected string $input_encoding = 'UTF-8';
protected string $output_encoding = 'UTF-8';
protected bool $skipBomSequence = false;
protected string $buffer = '';
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
$filter_name = self::FILTERNAME.'.*';
in_array($filter_name, stream_get_filters(), true) || stream_filter_register($filter_name, self::class);
}
/**
* Static method to add the stream filter to a {@link AbstractCsv} object.
*/
public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding, ?array $params = null): AbstractCsv
{
self::register();
if ($csv instanceof Reader) {
return $csv->appendStreamFilterOnRead(self::getFiltername($input_encoding, $output_encoding), $params);
}
return $csv->appendStreamFilterOnWrite(self::getFiltername($input_encoding, $output_encoding), $params);
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function appendOnReadTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::appendFilter($stream, STREAM_FILTER_READ, $input_encoding, $output_encoding);
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function appendOnWriteTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::appendFilter($stream, STREAM_FILTER_WRITE, $input_encoding, $output_encoding);
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function prependOnReadTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::prependFilter($stream, STREAM_FILTER_READ, $input_encoding, $output_encoding);
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function prependOnWriteTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::prependFilter($stream, STREAM_FILTER_WRITE, $input_encoding, $output_encoding);
}
/**
* @param resource $stream
*
* @throws RuntimeException|TypeError
*
* @return resource
*/
final protected static function appendFilter(mixed $stream, int $mode, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
self::register();
$filtername = self::getFiltername($input_encoding, $output_encoding);
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_append(self::filterStream($stream), $filtername, $mode);
restore_error_handler();
is_resource($filter) || throw new RuntimeException('Could not append the registered stream filter: '.$filtername);
return $filter;
}
/**
* @param resource $stream
*
* @throws RuntimeException|TypeError
*
* @return resource
*/
final protected static function prependFilter(mixed $stream, int $mode, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
self::register();
$filtername = self::getFiltername($input_encoding, $output_encoding);
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_prepend(self::filterStream($stream), $filtername, $mode);
restore_error_handler();
is_resource($filter) || throw new RuntimeException('Could not append the registered stream filter: '.$filtername);
return $filter;
}
/**
* @param resource $stream
*
* @throws TypeError
*
* @return resource
*/
final protected static function filterStream(mixed $stream): mixed
{
is_resource($stream) || throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.');
'stream' === ($type = get_resource_type($stream)) || throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given');
return $stream;
}
/**
* Static method to return the stream filter filtername.
*/
public static function getFiltername(string $input_encoding, string $output_encoding): string
{
return sprintf(
'%s.%s/%s',
self::FILTERNAME,
self::filterEncoding($input_encoding),
self::filterEncoding($output_encoding)
);
}
/**
* Filter encoding charset.
*
* @throws OutOfRangeException if the charset is malformed or unsupported
*/
final protected static function filterEncoding(string $encoding): string
{
static $encoding_list;
$encoding_list ??= array_reduce(mb_list_encodings(), fn (array $list, string $encoding): array => [...$list, ...[strtolower($encoding) => $encoding]], []);
return $encoding_list[strtolower($encoding)] ?? throw new OutOfRangeException('The submitted charset '.$encoding.' is not supported by the mbstring extension.');
}
public function onCreate(): bool
{
$prefix = self::FILTERNAME.'.';
if (!str_starts_with($this->filtername, $prefix)) {
return false;
}
$encodings = substr($this->filtername, strlen($prefix));
if (1 !== preg_match(',^(?<input>[-\w]+)/(?<output>[-\w]+)$,', $encodings, $matches)) {
return false;
}
try {
$this->input_encoding = self::filterEncoding($matches['input']);
$this->output_encoding = self::filterEncoding($matches['output']);
$this->skipBomSequence = is_array($this->params)
&& isset($this->params[self::BOM_SEQUENCE])
&& self::SKIP_BOM_SEQUENCE === $this->params[self::BOM_SEQUENCE];
} catch (OutOfRangeException) {
return false;
}
return true;
}
public function filter($in, $out, &$consumed, bool $closing): int
{
$inputBuffer = $this->buffer;
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$inputBuffer .= $bucket->data;
$consumed += $bucket->datalen;
}
if ('' === $inputBuffer && !$closing) {
return PSFS_FEED_ME;
}
if ($this->skipBomSequence && null !== ($bom = Bom::tryFromSequence($inputBuffer))) {
$inputBuffer = substr($inputBuffer, $bom->length());
}
// if the stream content is invalid then we store it and
// ask for more content to try to correctly convert the data
if (!mb_check_encoding($inputBuffer, $this->input_encoding) && !$closing) {
$this->buffer = $inputBuffer;
return PSFS_FEED_ME;
}
try {
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$outputBuffer = mb_convert_encoding($inputBuffer, $this->output_encoding, $this->input_encoding);
$streamBucket = stream_bucket_new($this->stream, $outputBuffer);
stream_bucket_append($out, $streamBucket);
return PSFS_PASS_ON;
} catch (Throwable) {
return PSFS_ERR_FATAL;
} finally {
$this->buffer = '';
restore_error_handler();
}
}
/**
* Converts Csv records collection into UTF-8.
*/
public function convert(iterable $records): iterable
{
return match (true) {
$this->output_encoding === $this->input_encoding => $records,
is_array($records) => array_map($this, $records),
default => MapIterator::fromIterable($records, $this),
};
}
/**
* Enable using the class as a formatter for the {@link Writer}.
*/
public function __invoke(array $record): array
{
$outputRecord = [];
foreach ($record as $offset => $value) {
[$newOffset, $newValue] = $this->encodeField($value, $offset);
$outputRecord[$newOffset] = $newValue;
}
return $outputRecord;
}
/**
* Walker method to convert the offset and the value of a CSV record field.
*/
final protected function encodeField(int|float|string|null $value, int|string $offset): array
{
if (null !== $value && !is_numeric($value)) {
$value = mb_convert_encoding($value, $this->output_encoding, $this->input_encoding);
}
if (!is_numeric($offset)) {
$offset = mb_convert_encoding($offset, $this->output_encoding, $this->input_encoding);
}
return [$offset, $value];
}
/**
* Sets the records input encoding charset.
*/
public function inputEncoding(string $encoding): self
{
$encoding = self::filterEncoding($encoding);
if ($encoding === $this->input_encoding) {
return $this;
}
$clone = clone $this;
$clone->input_encoding = $encoding;
return $clone;
}
/**
* Sets the records output encoding charset.
*/
public function outputEncoding(string $encoding): self
{
$encoding = self::filterEncoding($encoding);
if ($encoding === $this->output_encoding) {
return $this;
}
$clone = clone $this;
$clone->output_encoding = $encoding;
return $clone;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @codeCoverageIgnore
* @see self::appendOnReadTo()
* @see self::appendOnWriteTo()
* @deprecated since version 9.22.0
*
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
#[Deprecated(message:'use League\Csv\CharserConverter::appendOnReadTo() or League\Csv\CharserConverter::appendOnWriteTo() instead', since:'league/csv:9.22.0')]
public static function appendTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::appendFilter($stream, 0, $input_encoding, $output_encoding);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @codeCoverageIgnore
* @see self::prependOnReadTo()
* @see self::prependOnWriteTo()
* @deprecated since version 9.22.0
*
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
#[Deprecated(message:'use League\Csv\CharserConverter::prependOnReadTo() or League\Csv\CharserConverter::prependOnWriteTo() instead', since:'league/csv:9.22.0')]
public static function prependTo(mixed $stream, string $input_encoding = 'UTF-8', string $output_encoding = 'UTF-8'): mixed
{
return self::prependFilter($stream, 0, $input_encoding, $output_encoding);
}
/**
* Static method to add the stream filter to a {@link Reader} object to handle BOM skipping.
*/
public static function addBOMSkippingTo(Reader $document, string $output_encoding = 'UTF-8'): Reader
{
self::register();
$document->appendStreamFilterOnRead(
self::getFiltername((Bom::tryFrom($document->getInputBOM()) ?? Bom::Utf8)->encoding(), $output_encoding),
[self::BOM_SEQUENCE => self::SKIP_BOM_SEQUENCE]
);
return $document;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use function count;
/**
* Validates column consistency when inserting records into a CSV document.
*/
class ColumnConsistency
{
/**
* @throws InvalidArgument if the column count is less than -1
*/
public function __construct(
protected int $columns_count = -1
) {
$this->columns_count >= -1 || throw InvalidArgument::dueToInvalidColumnCount($this->columns_count, __METHOD__);
}
/**
* Returns the column count.
*/
public function getColumnCount(): int
{
return $this->columns_count;
}
/**
* Tells whether the submitted record is valid.
*/
public function __invoke(array $record): bool
{
$count = count($record);
if (-1 === $this->columns_count) {
$this->columns_count = $count;
return true;
}
return $count === $this->columns_count;
}
}

144
vendor/league/csv/src/EncloseField.php vendored Normal file
View File

@ -0,0 +1,144 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use InvalidArgumentException;
use php_user_filter;
use Throwable;
use function array_map;
use function in_array;
use function restore_error_handler;
use function set_error_handler;
use function str_replace;
use function strcspn;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_bucket_new;
use function stream_filter_register;
use function stream_get_filters;
use function strlen;
use function trigger_error;
use const E_USER_WARNING;
use const PSFS_ERR_FATAL;
use const PSFS_PASS_ON;
/**
* A stream filter to improve enclosure character usage.
*
* DEPRECATION WARNING! This class will be removed in the next major point release
*
* @deprecated since version 9.10.0
* @see Writer::forceEnclosure()
*
* @see https://tools.ietf.org/html/rfc4180#section-2
* @see https://bugs.php.net/bug.php?id=38301
*/
class EncloseField extends php_user_filter
{
#[Deprecated(message: 'use League\Csv\Writer::forceEnclosure() instead', since: 'league/csv:9.10.0')]
public const FILTERNAME = 'convert.league.csv.enclosure';
/** Default sequence. */
protected string $sequence = '';
/** Characters that triggers enclosure in PHP. */
protected static string $force_enclosure = "\n\r\t ";
/**
* Static method to return the stream filter filtername.
*/
public static function getFiltername(): string
{
return self::FILTERNAME;
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
if (!in_array(self::FILTERNAME, stream_get_filters(), true)) {
stream_filter_register(self::FILTERNAME, self::class);
}
}
/**
* Static method to add the stream filter to a {@link Writer} object.
*
* @throws InvalidArgumentException if the sequence is malformed
* @throws Exception
*/
public static function addTo(Writer $csv, string $sequence): Writer
{
self::register();
if (!self::isValidSequence($sequence)) {
throw new InvalidArgumentException('The sequence must contain at least one character to force enclosure');
}
return $csv
->addFormatter(fn (array $record): array => array_map(fn (?string $value): string => $sequence.$value, $record))
->addStreamFilter(self::FILTERNAME, ['sequence' => $sequence]);
}
/**
* Filter type and sequence parameters.
*
* The sequence to force enclosure MUST contain one of the following character ("\n\r\t ")
*/
protected static function isValidSequence(string $sequence): bool
{
return strlen($sequence) !== strcspn($sequence, self::$force_enclosure);
}
#[Deprecated(message: 'use League\Csv\Writer::forceEnclosure() instead', since: 'league/csv:9.10.0')]
public function onCreate(): bool
{
return is_array($this->params)
&& isset($this->params['sequence'])
&& self::isValidSequence($this->params['sequence']);
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
*/
public function filter($in, $out, &$consumed, bool $closing): int
{
$data = '';
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$data .= $bucket->data;
$consumed += $bucket->datalen;
}
/** @var array $params */
$params = $this->params;
try {
$data = str_replace($params['sequence'], '', $data);
} catch (Throwable $exception) {
trigger_error('An error occurred while executing the stream filter `'.$this->filtername.'`: '.$exception->getMessage(), E_USER_WARNING);
return PSFS_ERR_FATAL;
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
stream_bucket_append($out, stream_bucket_new($this->stream, $data));
restore_error_handler();
return PSFS_PASS_ON;
}
}

160
vendor/league/csv/src/EscapeFormula.php vendored Normal file
View File

@ -0,0 +1,160 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use InvalidArgumentException;
use Stringable;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function is_string;
/**
* A Formatter to tackle CSV Formula Injection.
*
* @see http://georgemauer.net/2017/10/07/csv-injection.html
*/
class EscapeFormula
{
/** Spreadsheet formula starting character. */
public const FORMULA_STARTING_CHARS = ['=', '-', '+', '@', "\t", "\r"];
/** Effective Spreadsheet formula starting characters. */
protected array $special_chars = [];
/**
* @param string $escape escape character to escape each CSV formula field
* @param array<string> $special_chars additional spreadsheet formula starting characters
*/
public function __construct(
protected string $escape = "'",
array $special_chars = []
) {
$this->special_chars = array_fill_keys([
...self::FORMULA_STARTING_CHARS,
...$this->filterSpecialCharacters(...$special_chars),
], 1);
}
/**
* Filter submitted special characters.
*
* @throws InvalidArgumentException if the string is not a single character
*
* @return array<string>
*/
protected function filterSpecialCharacters(string ...$characters): array
{
foreach ($characters as $str) {
1 === strlen($str) || throw new InvalidArgumentException('The submitted string '.$str.' must be a single character');
}
return $characters;
}
/**
* Returns the list of character the instance will escape.
*
* @return array<string>
*/
public function getSpecialCharacters(): array
{
return array_keys($this->special_chars);
}
/**
* Returns the escape character.
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* Escapes a CSV record.
*/
public function escapeRecord(array $record): array
{
return array_map($this->escapeField(...), $record);
}
public function unescapeRecord(array $record): array
{
return array_map($this->unescapeField(...), $record);
}
/**
* Escapes a CSV cell if its content is stringable.
*/
protected function escapeField(mixed $cell): mixed
{
$strOrNull = match (true) {
is_string($cell) => $cell,
$cell instanceof Stringable => (string) $cell,
default => null,
};
return match (true) {
null == $strOrNull,
!isset($strOrNull[0], $this->special_chars[$strOrNull[0]]) => $cell,
default => $this->escape.$strOrNull,
};
}
protected function unescapeField(mixed $cell): mixed
{
$strOrNull = match (true) {
is_string($cell) => $cell,
$cell instanceof Stringable => (string) $cell,
default => null,
};
return match (true) {
null === $strOrNull,
!isset($strOrNull[0], $strOrNull[1]),
$strOrNull[0] !== $this->escape,
!isset($this->special_chars[$strOrNull[1]]) => $cell,
default => substr($strOrNull, 1),
};
}
/**
* @deprecated since 9.7.2 will be removed in the next major release
* @codeCoverageIgnore
*
* Tells whether the submitted value is stringable.
*
* @param mixed $value value to check if it is stringable
*/
protected function isStringable(mixed $value): bool
{
return is_string($value) || $value instanceof Stringable;
}
/**
* @deprecated since 9.11.0 will be removed in the next major release
* @codeCoverageIgnore
*
* League CSV formatter hook.
*
* @see escapeRecord
*/
#[Deprecated(message:'use League\Csv\EscapeFormula::escapeRecord() instead', since:'league/csv:9.11.0')]
public function __invoke(array $record): array
{
return $this->escapeRecord($record);
}
}

23
vendor/league/csv/src/Exception.php vendored Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Exception as PhpException;
/**
* League Csv Base Exception.
*/
class Exception extends PhpException implements UnableToProcessCsv
{
}

428
vendor/league/csv/src/FragmentFinder.php vendored Normal file
View File

@ -0,0 +1,428 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use function array_filter;
use function array_map;
use function array_reduce;
use function count;
use function explode;
use function filter_var;
use function preg_match;
use function range;
use const FILTER_VALIDATE_INT;
/**
* EXPERIMENTAL WARNING! This class implementation will change in the next major point release.
*
* @phpstan-type selection array{selection:string, start:int<-1, max>, end:?int, length:int, columns:array<int>}
*
* @experimental
*/
class FragmentFinder
{
private const REGEXP_URI_FRAGMENT = ',^(?<type>row|cell|col)=(?<selections>.*)$,i';
private const REGEXP_ROWS_COLUMNS_SELECTION = '/^(?<start>\d+)(-(?<end>\d+|\*))?$/';
private const REGEXP_CELLS_SELECTION = '/^(?<csr>\d+),(?<csc>\d+)(-(?<end>((?<cer>\d+),(?<cec>\d+))|\*))?$/';
private const TYPE_ROW = 'row';
private const TYPE_COLUMN = 'col';
private const TYPE_UNKNOWN = 'unknown';
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract all found fragment identifiers for the specifield tabular data
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @return iterable<int, TabularDataReader>
*/
public function findAll(string $expression, TabularData $tabularData): iterable
{
return $this->find($this->parseExpression($expression, $tabularData), $tabularData);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or returns null
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
*/
public function findFirst(string $expression, TabularData $tabularData): ?TabularDataReader
{
$fragment = $this->find($this->parseExpression($expression, $tabularData), $tabularData)[0];
return match ([]) {
$fragment->first() => null,
default => $fragment,
};
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or fail
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @throws FragmentNotFound if the expression can not be parsed
*/
public function findFirstOrFail(string $expression, TabularData $tabularData): TabularDataReader
{
$parsedExpression = $this->parseExpression($expression, $tabularData);
if ([] !== array_filter($parsedExpression['selections'], fn (array $selection) => -1 === $selection['start'])) {
throw new FragmentNotFound('The expression `'.$expression.'` contains an invalid or an unsupported selection for the tabular data.');
}
$fragment = $this->find($parsedExpression, $tabularData)[0];
return match ([]) {
$fragment->first() => throw new FragmentNotFound('No fragment found in the tabular data with the expression `'.$expression.'`.'),
default => $fragment,
};
}
/**
* @param array{type:string, selections:non-empty-array<selection>} $parsedExpression
*
* @throws SyntaxError
*
* @return array<int, TabularDataReader>
*/
private function find(array $parsedExpression, TabularData $tabularData): array
{
['type' => $type, 'selections' => $selections] = $parsedExpression;
$selections = array_filter($selections, fn (array $selection) => -1 !== $selection['start']);
if ([] === $selections) {
return [new ResultSet()];
}
if (self::TYPE_ROW === $type) {
$rowFilter = fn (array $record, int $offset): bool => [] !== array_filter(
$selections,
fn (array $selection) =>
$offset >= $selection['start'] &&
(null === $selection['end'] || $offset <= $selection['end'])
);
return [
(new Statement())
->where($rowFilter)
->process($tabularData),
];
}
if (self::TYPE_COLUMN === $type) {
$columns = array_reduce(
$selections,
fn (array $columns, array $selection) => [...$columns, ...$selection['columns']],
[]
);
return [match ([]) {
$columns => new ResultSet(),
default => (new Statement())->select(...$columns)->process($tabularData),
}];
}
return array_map(
fn (array $selection) => (new Statement())
->offset($selection['start'])
->limit($selection['length'])
->select(...$selection['columns'])
->process($tabularData),
$selections
);
}
/**
* @return array{type:string, selections:non-empty-array<selection>}
*/
private function parseExpression(string $expression, TabularData $tabularData): array
{
if (1 !== preg_match(self::REGEXP_URI_FRAGMENT, $expression, $matches)) {
return [
'type' => self::TYPE_UNKNOWN,
'selections' => [
[
'selection' => $expression,
'start' => -1,
'end' => null,
'length' => -1,
'columns' => [],
],
],
];
}
$type = strtolower($matches['type']);
/** @var non-empty-array<selection> $res */
$res = array_reduce(
explode(';', $matches['selections']),
fn (array $selections, string $selection): array => [...$selections, match ($type) {
self::TYPE_ROW => $this->parseRowSelection($selection),
self::TYPE_COLUMN => $this->parseColumnSelection($selection, $tabularData),
default => $this->parseCellSelection($selection, $tabularData),
}],
[]
);
return [
'type' => $type,
'selections' => $res,
];
}
/**
* @return selection
*/
private function parseRowSelection(string $selection): array
{
[$start, $end] = $this->parseRowColumnSelection($selection);
return match (true) {
-1 === $start,
null === $end => [
'selection' => $selection,
'start' => $start,
'end' => $start,
'length' => 1,
'columns' => [],
],
'*' === $end => [
'selection' => $selection,
'start' => $start,
'end' => null,
'length' => -1,
'columns' => [],
],
default => [
'selection' => $selection,
'start' => $start,
'end' => $end,
'length' => $end - $start + 1,
'columns' => [],
],
};
}
/**
* @return selection
*/
private function parseColumnSelection(string $selection, TabularData $tabularData): array
{
[$start, $end] = $this->parseRowColumnSelection($selection);
$header = $tabularData->getHeader();
if ([] === $header) {
$header = $tabularData->nth(0);
}
$nbColumns = count($header);
return match (true) {
-1 === $start,
$start >= $nbColumns => [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => -1,
'columns' => [],
],
null === $end => [
'selection' => $selection,
'start' => 0,
'end' => null,
'length' => -1,
'columns' => [$start],
],
'*' === $end,
$end > ($nbColumns - 1) => [
'selection' => $selection,
'start' => 0,
'end' => null,
'length' => -1,
'columns' => range($start, $nbColumns - 1),
],
default => [
'selection' => $selection,
'start' => 0,
'end' => $end,
'length' => -1,
'columns' => range($start, $end),
],
};
}
/**
* @return array{int<-1, max>, int|null|'*'}
*/
private function parseRowColumnSelection(string $selection): array
{
if (1 !== preg_match(self::REGEXP_ROWS_COLUMNS_SELECTION, $selection, $found)) {
return [-1, 0];
}
$start = $found['start'];
$end = $found['end'] ?? null;
$start = filter_var($start, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $start) {
return [-1, 0];
}
--$start;
if (null === $end || '*' === $end) {
return [$start, $end];
}
$end = filter_var($end, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $end) {
return [-1, 0];
}
--$end;
if ($end <= $start) {
return [-1, 0];
}
return [$start, $end];
}
/**
* @return selection
*/
private function parseCellSelection(string $selection, TabularData $tabularData): array
{
if (1 !== preg_match(self::REGEXP_CELLS_SELECTION, $selection, $found)) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
$cellStartRow = filter_var($found['csr'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$cellStartCol = filter_var($found['csc'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $cellStartRow || false === $cellStartCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
--$cellStartRow;
--$cellStartCol;
$header = $tabularData->getHeader();
if ([] === $header) {
$header = $tabularData->nth(0);
}
$nbColumns = count($header);
if ($cellStartCol > $nbColumns - 1) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
$cellEnd = $found['end'] ?? null;
if (null === $cellEnd) {
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => null,
'length' => 1,
'columns' => [$cellStartCol],
];
}
if ('*' === $cellEnd) {
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => null,
'length' => -1,
'columns' => range($cellStartCol, $nbColumns - 1),
];
}
$cellEndRow = filter_var($found['cer'] ?? '', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$cellEndCol = filter_var($found['cec'] ?? '', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $cellEndRow || false === $cellEndCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
--$cellEndRow;
--$cellEndCol;
if ($cellEndRow < $cellStartRow || $cellEndCol < $cellStartCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => $cellEndRow,
'length' => $cellEndRow - $cellStartRow + 1,
'columns' => range($cellStartCol, ($cellEndCol > $nbColumns - 1) ? $nbColumns - 1 : $cellEndCol),
];
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see FragmentFinder::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*
* Returns a new instance.
*/
#[Deprecated(message:'use League\Csv\FragmentFinder::__construct()', since:'league/csv:9.22.0')]
public static function create(): self
{
return new self();
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use RuntimeException;
final class FragmentNotFound extends RuntimeException implements UnableToProcessCsv
{
}

245
vendor/league/csv/src/HTMLConverter.php vendored Normal file
View File

@ -0,0 +1,245 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use Deprecated;
use Dom\HTMLDocument;
use Dom\HTMLElement;
use Dom\XMLDocument;
use DOMDocument;
use DOMElement;
use DOMException;
use function is_bool;
use function preg_match;
/**
* Converts tabular data into an HTML Table string.
*/
class HTMLConverter
{
/** table class attribute value. */
protected string $class_name = 'table-csv-data';
/** table id attribute value. */
protected string $id_value = '';
/** @var ?Closure(array, array-key): array */
protected ?Closure $formatter = null;
protected string $offset_attr = '';
protected string $column_attr = '';
private static function supportsModernDom(): bool
{
return extension_loaded('dom') && class_exists(HTMLDocument::class);
}
public function __construct()
{
}
/**
* Converts a tabular data collection into an HTML table string.
*
* @param array<string> $header_record An optional array of headers outputted using the `<thead>` and `<th>` elements
* @param array<string> $footer_record An optional array of footers outputted using the `<tfoot>` and `<th>` elements
*/
public function convert(iterable $records, array $header_record = [], array $footer_record = []): string
{
if (null !== $this->formatter) {
$records = MapIterator::fromIterable($records, $this->formatter);
}
$document = self::supportsModernDom() ? HTMLDocument::createEmpty() : new DOMDocument('1.0');
$table = $document->createElement('table');
if ('' !== $this->class_name) {
$table->setAttribute('class', $this->class_name);
}
if ('' !== $this->id_value) {
$table->setAttribute('id', $this->id_value);
}
$this->appendHeaderSection('thead', $header_record, $table);
$this->appendHeaderSection('tfoot', $footer_record, $table);
$tbody = $table;
if ($table->hasChildNodes()) {
$tbody = $document->createElement('tbody');
$table->appendChild($tbody);
}
foreach ($records as $offset => $record) {
$tr = $document->createElement('tr');
if ('' !== $this->offset_attr) {
$tr->setAttribute($this->offset_attr, (string) $offset);
}
foreach ($record as $field_name => $field_value) {
$td = $document->createElement('td');
if ('' !== $this->column_attr) {
$td->setAttribute($this->column_attr, (string) $field_name);
}
$td->appendChild($document->createTextNode($field_value));
$tr->appendChild($td);
}
$tbody->appendChild($tr);
}
$document->appendChild($table);
return (string) $document->saveHTML($table);
}
/**
* Creates a DOMElement representing an HTML table heading section.
*
* @throws DOMException
*/
protected function appendHeaderSection(string $node_name, array $record, DOMElement|HTMLElement $table): void
{
if ([] === $record) {
return;
}
/** @var DOMDocument|HTMLDocument $document */
$document = $table->ownerDocument;
$header = $document->createElement($node_name);
$tr = $document->createElement('tr');
foreach ($record as $field_value) {
$th = $document->createElement('th');
$th->setAttribute('scope', 'col');
$th->appendChild($document->createTextNode($field_value));
$tr->appendChild($th);
}
$header->appendChild($tr);
$table->appendChild($header);
}
/**
* HTML table class name setter.
*
* @throws DOMException if the id_value contains any type of whitespace
*/
public function table(string $class_name, string $id_value = ''): self
{
1 !== preg_match(",\s,", $id_value) || throw new DOMException("The id attribute's value must not contain whitespace (spaces, tabs etc.)");
$clone = clone $this;
$clone->class_name = $class_name;
$clone->id_value = $id_value;
return $clone;
}
/**
* HTML tr record offset attribute setter.
*/
public function tr(string $record_offset_attribute_name): self
{
if ($record_offset_attribute_name === $this->offset_attr) {
return $this;
}
if (!self::filterAttributeNme($record_offset_attribute_name)) {
throw new DOMException('The submitted attribute name `'.$record_offset_attribute_name.'` is not valid.');
}
$clone = clone $this;
$clone->offset_attr = $record_offset_attribute_name;
return $clone;
}
/**
* HTML td field name attribute setter.
*/
public function td(string $fieldname_attribute_name): self
{
if ($fieldname_attribute_name === $this->column_attr) {
return $this;
}
if (!self::filterAttributeNme($fieldname_attribute_name)) {
throw new DOMException('The submitted attribute name `'.$fieldname_attribute_name.'` is not valid.');
}
$clone = clone $this;
$clone->column_attr = $fieldname_attribute_name;
return $clone;
}
private static function filterAttributeNme(string $attribute_name): bool
{
try {
$document = self::supportsModernDom() ? XmlDocument::createEmpty() : new DOMDocument('1.0');
$div = $document->createElement('div');
$div->setAttribute($attribute_name, 'foo');
return true;
} catch (DOMException) {
return false;
}
}
/**
* Set a callback to format each item before json encode.
*
* @param ?callable(array, array-key): array $formatter
*/
public function formatter(?callable $formatter): self
{
$clone = clone $this;
$clone->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);
return $clone;
}
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see XMLConverter::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*
* Returns an new instance.
*/
#[Deprecated(message:'use League\Csv\HTMLConverter::__construct()', since:'league/csv:9.22.0')]
public static function create(): self
{
return new self();
}
}

64
vendor/league/csv/src/HttpHeaders.php vendored Normal file
View File

@ -0,0 +1,64 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use InvalidArgumentException;
use function filter_var;
use function header;
use function rawurlencode;
use function str_contains;
use function str_replace;
use const FILTER_FLAG_STRIP_HIGH;
use const FILTER_FLAG_STRIP_LOW;
/**
* Send the CSV headers.
*
* Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
*
* @see https://tools.ietf.org/html/rfc6266#section-4.3
*
* @internal
*/
final class HttpHeaders
{
/**
* @throws InvalidArgumentException
*/
public static function forFileDownload(string $filename, string $contentType): void
{
if (str_contains($filename, '/') || str_contains($filename, '\\')) {
throw new InvalidArgumentException('The filename `'.$filename.'` cannot contain the "/" or "\" characters.');
}
/** @var string $filteredName */
$filteredName = filter_var($filename, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
$fallbackName = str_replace('%', '', $filteredName);
$disposition = 'attachment;filename="'.str_replace('"', '\\"', $fallbackName).'"';
if ($filename !== $fallbackName) {
$disposition .= ";filename*=UTF-8''".preg_replace_callback(
'/[%"\x00-\x1F\x7F-\xFF]/',
static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
$filename
);
}
header('content-type: '.$contentType);
header('content-transfer-encoding: binary');
header('content-description: File Transfer');
header('content-disposition: '.$disposition);
}
}

81
vendor/league/csv/src/Info.php vendored Normal file
View File

@ -0,0 +1,81 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use function array_fill_keys;
use function array_filter;
use function array_reduce;
use function array_unique;
use function count;
use function strlen;
use const COUNT_RECURSIVE;
final class Info implements ByteSequence
{
/**
* Returns the BOM sequence found at the start of the string.
*
* If no valid BOM sequence is found an empty string is returned
*
* @deprecated since version 9.16.0
* @see Bom::tryFromSequence()
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\Bom::tryFromSequence() instead', since:'league/csv:9.16.0')]
public static function fetchBOMSequence(string $str): ?string
{
return Bom::tryFromSequence($str)?->value;
}
/**
* Detect Delimiters usage in a {@link Reader} object.
*
* Returns a associative array where each key represents
* a submitted delimiter and each value the number CSV fields found
* when processing at most $limit CSV records with the given delimiter
*
* @param array<string> $delimiters
* @param int<-1, max> $limit
*
* @return array<string, int>
*/
public static function getDelimiterStats(Reader $csv, array $delimiters, int $limit = 1): array
{
$currentHeaderOffset = $csv->getHeaderOffset();
$currentDelimiter = $csv->getDelimiter();
$stats = array_reduce(
array_unique(array_filter($delimiters, fn (string $value): bool => 1 === strlen($value))),
fn (array $stats, string $delimiter): array => [
...$stats,
...[$delimiter => count([
...$csv
->setHeaderOffset(null)
->setDelimiter($delimiter)
->slice(0, $limit)
->filter(fn (array $record, int|string $key): bool => 1 < count($record)),
], COUNT_RECURSIVE)],
],
array_fill_keys($delimiters, 0)
);
$csv->setHeaderOffset($currentHeaderOffset);
$csv->setDelimiter($currentDelimiter);
return $stats;
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use Throwable;
/**
* InvalidArgument Exception.
*/
class InvalidArgument extends Exception
{
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToInvalidChunkSize(int $length, string $method): self
{
return new self($method.'() expects the length to be a positive integer '.$length.' given.');
}
public static function dueToInvalidHeaderFilename(string $filename): self
{
return new self('The filename `'.$filename.'` cannot contain the "/" and "\\" characters.');
}
public static function dueToInvalidDelimiterCharacter(string $delimiter, string $method): self
{
return new self($method.'() expects delimiter to be a single character; `'.$delimiter.'` given.');
}
public static function dueToInvalidEnclosureCharacter(string $enclosure, string $method): self
{
return new self($method.'() expects enclosure to be a single character; `'.$enclosure.'` given.');
}
public static function dueToInvalidEscapeCharacter(string $escape, string $method): self
{
return new self($method.'() expects escape to be a single character or an empty string; `'.$escape.'` given.');
}
public static function dueToInvalidBOMCharacter(string $method, Throwable $exception): self
{
return new self($method.'() expects a valid Byte Order Mark.', 0, $exception);
}
public static function dueToInvalidColumnCount(int $columns_count, string $method): self
{
return new self($method.'() expects the column count to be greater or equal to -1 '.$columns_count.' given.');
}
public static function dueToInvalidHeaderOffset(int $offset, string $method): self
{
return new self($method.'() expects header offset to be greater or equal to 0; `'.$offset.'` given.');
}
public static function dueToInvalidRecordOffset(int $offset, string $method): self
{
return new self($method.'() expects the submitted offset to be a positive integer or 0, '.$offset.' given');
}
public static function dueToInvalidColumnIndex(string|int $index, string $type, string $method): self
{
return new self($method.'() expects the '.$type.' index to be a valid string or integer, `'.$index.'` given');
}
public static function dueToInvalidLimit(int $limit, string $method): self
{
return new self($method.'() expects the limit to be greater or equal to -1, '.$limit.' given.');
}
public static function dueToInvalidOrder(string $order, string $method): self
{
return new self($method.'() expects `ASC` or `DESC` in a case-insensitive way, '.$order.' given.');
}
public static function dueToInvalidOperator(string $operator, string $method): self
{
return new self($method.'() expects valid comparison operator in a case-insensitive way, '.$operator.' given.');
}
public static function dueToInvalidSeekingPosition(int $position, string $method): self
{
return new self($method.'() can\'t seek stream to negative line '.$position);
}
public static function dueToStreamFilterNotFound(string $filtername): self
{
return new self('unable to locate filter `'.$filtername.'`');
}
public static function dueToInvalidThreshold(int $threshold, string $method): self
{
return new self($method.'() expects threshold to be null or a valid integer greater or equal to 1');
}
}

542
vendor/league/csv/src/JsonConverter.php vendored Normal file
View File

@ -0,0 +1,542 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use BadMethodCallException;
use Closure;
use Deprecated;
use Exception;
use InvalidArgumentException;
use Iterator;
use JsonException;
use RuntimeException;
use SplFileInfo;
use SplFileObject;
use TypeError;
use function array_filter;
use function array_reduce;
use function get_defined_constants;
use function is_bool;
use function is_resource;
use function is_string;
use function json_encode;
use function json_last_error;
use function preg_match;
use function restore_error_handler;
use function set_error_handler;
use function str_repeat;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strtolower;
use function substr;
use function ucwords;
use const ARRAY_FILTER_USE_KEY;
use const JSON_ERROR_NONE;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
/**
* Converts and store tabular data into a JSON string.
* @template T
*
* @method JsonConverter withHexTag() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexTag() removes the JSON_HEX_TAG flag
* @method bool useHexTag() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexAmp() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexAmp() removes the JSON_HEX_TAG flag
* @method bool useHexAmp() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexApos() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexApos() removes the JSON_HEX_TAG flag
* @method bool useHexApos() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexQuot() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexQuot() removes the JSON_HEX_TAG flag
* @method bool useHexQuot() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withForceObject() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutForceObject() removes the JSON_HEX_TAG flag
* @method bool useForceObject() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withNumericCheck() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutNumericCheck() removes the JSON_HEX_TAG flag
* @method bool useNumericCheck() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedSlashes() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedSlashes() removes the JSON_HEX_TAG flag
* @method bool useUnescapedSlashes() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withoutPrettyPrint() removes the JSON_PRETTY_PRINT flag
* @method bool usePrettyPrint() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedUnicode() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedUnicode() removes the JSON_HEX_TAG flag
* @method bool useUnescapedUnicode() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withPartialOutputOnError() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutPartialOutputOnError() removes the JSON_HEX_TAG flag
* @method bool usePartialOutputOnError() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withPreserveZeroFraction() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutPreserveZeroFraction() removes the JSON_HEX_TAG flag
* @method bool usePreserveZeroFraction() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedLineTerminators() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedLineTerminators() removes the JSON_HEX_TAG flag
* @method bool useUnescapedLineTerminators() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withInvalidUtf8Ignore() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutInvalidUtf8Ignore() removes the JSON_HEX_TAG flag
* @method bool useInvalidUtf8Ignore() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withInvalidUtf8Substitute() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutInvalidUtf8Substitute() removes the JSON_HEX_TAG flag
* @method bool useInvalidUtf8Substitute() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withThrowOnError() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutThrowOnError() removes the JSON_HEX_TAG flag
* @method bool useThrowOnError() tells whether the JSON_HEX_TAG flag is used
*/
final class JsonConverter
{
public readonly int $flags;
/** @var int<1, max> */
public readonly int $depth;
/** @var int<1, max> */
public readonly int $indentSize;
/** @var ?Closure(T, array-key): mixed */
public readonly ?Closure $formatter;
/** @var int<1, max> */
public readonly int $chunkSize;
/** @var non-empty-string */
private readonly string $start;
/** @var non-empty-string */
private readonly string $end;
/** @var non-empty-string */
private readonly string $separator;
/** @var non-empty-string */
private readonly string $emptyIterable;
/** @var non-empty-string */
private readonly string $indentation;
/** @var Closure(array<int, T>): string */
private readonly Closure $jsonEncodeChunk;
/** @var array<string> */
private array $indentationLevels = [];
/**
* @param int<1, max> $depth
* @param int<1, max> $indentSize
* @param ?callable(T, array-key): mixed $formatter
* @param int<1, max> $chunkSize
*
* @throws InvalidArgumentException
*/
public function __construct(
int $flags = 0,
int $depth = 512,
int $indentSize = 4,
?callable $formatter = null,
int $chunkSize = 500
) {
json_encode([], $flags & ~JSON_THROW_ON_ERROR, $depth);
JSON_ERROR_NONE === ($errorCode = json_last_error()) || throw new InvalidArgumentException('The flags or the depth given are not valid JSON encoding parameters in PHP; '.json_last_error_msg(), $errorCode);
1 <= $indentSize || throw new InvalidArgumentException('The indentation space must be greater or equal to 1.');
1 <= $chunkSize || throw new InvalidArgumentException('The chunk size must be greater or equal to 1.');
$this->flags = $flags;
$this->depth = $depth;
$this->indentSize = $indentSize;
$this->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);
$this->chunkSize = $chunkSize;
// Initialize settings and closure to use for conversion.
// To speed up the process we pre-calculate them
$this->indentation = str_repeat(' ', $this->indentSize);
$start = '[';
$end = ']';
$separator = ',';
$chunkFormatter = array_values(...);
$prettyPrintFormatter = fn (string $json): string => $json;
if ($this->useForceObject()) {
$start = '{';
$end = '}';
$chunkFormatter = fn (array $value): array => $value;
}
$this->emptyIterable = $start.$end;
if ($this->usePrettyPrint()) {
$start .= "\n";
$end = "\n".$end;
$separator .= "\n";
$prettyPrintFormatter = $this->prettyPrint(...);
}
$flags = ($this->flags & ~JSON_PRETTY_PRINT) | JSON_THROW_ON_ERROR;
$this->start = $start;
$this->end = $end;
$this->separator = $separator;
$this->jsonEncodeChunk = fn (array $chunk): string => ($prettyPrintFormatter)(substr(
string: json_encode(($chunkFormatter)($chunk), $flags, $this->depth), /* @phpstan-ignore-line */
offset: 1,
length: -1
));
}
/**
* Pretty Print the JSON string without using JSON_PRETTY_PRINT
* The method also allow using an arbitrary length for the indentation.
*/
private function prettyPrint(string $json): string
{
$level = 1;
$inQuotes = false;
$escape = false;
$length = strlen($json);
$str = [$this->indentation];
for ($i = 0; $i < $length; $i++) {
$char = $json[$i];
if ('"' === $char && !$escape) {
$inQuotes = !$inQuotes;
}
$escape = '\\' === $char && !$escape;
$str[] = $inQuotes ? $char : match ($char) {
'{', '[' => $char.($this->indentationLevels[++$level] ??= "\n".str_repeat($this->indentation, $level)),
'}', ']' => ($this->indentationLevels[--$level] ??= "\n".str_repeat($this->indentation, $level)).$char,
',' => $char.($this->indentationLevels[$level] ??= "\n".str_repeat($this->indentation, $level)),
':' => $char.' ',
default => $char,
};
}
return implode('', $str);
}
/**
* @throws BadMethodCallException
*/
public function __call(string $name, array $arguments): self|bool
{
return match (true) {
str_starts_with($name, 'without') => $this->removeFlags(self::methodToFlag($name, 7)),
str_starts_with($name, 'with') => $this->addFlags(self::methodToFlag($name, 4)),
str_starts_with($name, 'use') => $this->useFlags(self::methodToFlag($name, 3)),
default => throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.'),
};
}
/**
* @param int<1, max>|null $indentSize
*/
public function withPrettyPrint(?int $indentSize = null): self
{
$flags = $this->flags | JSON_PRETTY_PRINT;
$indentSize = $indentSize ?? $this->indentSize;
return match (true) {
$flags === $this->flags && $indentSize === $this->indentSize => $this,
default => new self($flags, $this->depth, $indentSize, $this->formatter, $this->chunkSize),
};
}
/**
* Returns the PHP json flag associated to its method suffix to ease method lookup.
*/
private static function methodToFlag(string $method, int $prefixSize): int
{
static $suffix2Flag;
if (null === $suffix2Flag) {
$suffix2Flag = [];
/** @var array<string, int> $jsonFlags */
$jsonFlags = get_defined_constants(true)['json'];
$jsonEncodeFlags = array_filter(
$jsonFlags,
fn (string $key) => 1 !== preg_match('/^(JSON_BIGINT_AS_STRING|JSON_OBJECT_AS_ARRAY|JSON_ERROR_)(.*)?$/', $key),
ARRAY_FILTER_USE_KEY
);
foreach ($jsonEncodeFlags as $name => $value) {
$suffix2Flag[str_replace('_', '', ucwords(strtolower(substr($name, 5)), '_'))] = $value;
}
}
return $suffix2Flag[substr($method, $prefixSize)]
?? throw new BadMethodCallException('The method "'.self::class.'::'.$method.'" does not exist.');
}
/**
* Adds a list of JSON flags.
*/
public function addFlags(int ...$flags): self
{
return $this->setFlags(
array_reduce($flags, fn (int $carry, int $flag): int => $carry | $flag, $this->flags)
);
}
/**
* Removes a list of JSON flags.
*/
public function removeFlags(int ...$flags): self
{
return $this->setFlags(
array_reduce($flags, fn (int $carry, int $flag): int => $carry & ~$flag, $this->flags)
);
}
/**
* Tells whether the flag is being used by the current JsonConverter.
*/
public function useFlags(int ...$flags): bool
{
foreach ($flags as $flag) {
// the JSON_THROW_ON_ERROR flag is always used even if it is not set by the user
if (JSON_THROW_ON_ERROR !== $flag && ($this->flags & $flag) !== $flag) {
return false;
}
}
return [] !== $flags;
}
/**
* Sets the encoding flags.
*/
private function setFlags(int $flags): self
{
return match ($flags) {
$this->flags => $this,
default => new self($flags, $this->depth, $this->indentSize, $this->formatter, $this->chunkSize),
};
}
/**
* Set the depth of Json encoding.
*
* @param int<1, max> $depth
*/
public function depth(int $depth): self
{
return match ($depth) {
$this->depth => $this,
default => new self($this->flags, $depth, $this->indentSize, $this->formatter, $this->chunkSize),
};
}
/**
* Set the indentation size.
*
* @param int<1, max> $chunkSize
*/
public function chunkSize(int $chunkSize): self
{
return match ($chunkSize) {
$this->chunkSize => $this,
default => new self($this->flags, $this->depth, $this->indentSize, $this->formatter, $chunkSize),
};
}
/**
* Set a callback to format each item before json encode.
*/
public function formatter(?callable $formatter): self
{
return new self($this->flags, $this->depth, $this->indentSize, $formatter, $this->chunkSize);
}
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* Sends and makes the JSON structure downloadable via HTTP.
*.
* Returns the number of characters read from the handle and passed through to the output.
*
* @param iterable<T> $records
*
* @throws Exception
* @throws JsonException
*/
public function download(iterable $records, ?string $filename = null): int
{
if (null !== $filename) {
HttpHeaders::forFileDownload($filename, 'application/json; charset=utf-8');
}
return $this->save($records, new SplFileObject('php://output', 'wb'));
}
/**
* Returns the JSON representation of a tabular data collection.
*
* @param iterable<T> $records
*
* @throws Exception
* @throws JsonException
*/
public function encode(iterable $records): string
{
$stream = Stream::createFromString();
$this->save($records, $stream);
$stream->rewind();
return (string) $stream->getContents();
}
/**
* Store the generated JSON in the destination filepath.
*
* if a Path or a SplFileInfo object is given,
* the file will be emptying before adding the JSON
* content to it. For all the other types you are
* required to provide a file with the correct open
* mode.
*
* @param iterable<T> $records
* @param SplFileInfo|SplFileObject|Stream|resource|string $destination
* @param resource|null $context
*
* @throws JsonException
* @throws RuntimeException
* @throws TypeError
* @throws UnavailableStream
*/
public function save(iterable $records, mixed $destination, $context = null): int
{
$stream = match (true) {
$destination instanceof Stream,
$destination instanceof SplFileObject => $destination,
$destination instanceof SplFileInfo => $destination->openFile(mode:'wb', context: $context),
is_resource($destination) => Stream::createFromResource($destination),
is_string($destination) => Stream::createFromPath($destination, 'wb', $context),
default => throw new TypeError('The destination path must be a filename, a stream or a SplFileInfo object.'),
};
$bytes = 0;
$writtenBytes = 0;
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
foreach ($this->convert($records) as $line) {
if (false === ($writtenBytes = $stream->fwrite($line))) {
break;
}
$bytes += $writtenBytes;
}
restore_error_handler();
false !== $writtenBytes || throw new RuntimeException('Unable to write '.(isset($line) ? '`'.$line.'`' : '').' to the destination path `'.$stream->getPathname().'`.');
return $bytes;
}
/**
* Returns an Iterator that you can iterate to generate the actual JSON string representation.
*
* @param iterable<T> $records
*
* @throws JsonException
* @throws Exception
*
* @return Iterator<string>
*/
public function convert(iterable $records): Iterator
{
$iterator = match ($this->formatter) {
null => MapIterator::toIterator($records),
default => MapIterator::fromIterable($records, $this->formatter)
};
$iterator->rewind();
if (!$iterator->valid()) {
yield $this->emptyIterable;
return;
}
$chunk = [];
$chunkOffset = 0;
$offset = 0;
$current = $iterator->current();
$iterator->next();
yield $this->start;
while ($iterator->valid()) {
if ($chunkOffset === $this->chunkSize) {
yield ($this->jsonEncodeChunk)($chunk).$this->separator;
$chunkOffset = 0;
$chunk = [];
}
$chunk[$offset] = $current;
++$chunkOffset;
++$offset;
$current = $iterator->current();
$iterator->next();
}
if ([] !== $chunk) {
yield ($this->jsonEncodeChunk)($chunk).$this->separator;
}
yield ($this->jsonEncodeChunk)([$offset => $current]).$this->end;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see JsonConverter::withPrettyPrint()
* @deprecated Since version 9.19.0
* @codeCoverageIgnore
*
* Set the indentation size.
*
* @param int<1, max> $indentSize
*/
#[Deprecated(message:'use League\Csv\JsonConverter::withPrettyPrint() instead', since:'league/csv:9.19.0')]
public function indentSize(int $indentSize): self
{
return match ($indentSize) {
$this->indentSize => $this,
default => new self($this->flags, $this->depth, $indentSize, $this->formatter, $this->chunkSize),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see JsonConverter::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\JsonConverter::__construct() instead', since:'league/csv:9.22.0')]
public static function create(): self
{
return new self(
flags: 0,
depth: 512,
indentSize: 4,
formatter: null,
chunkSize: 500
);
}
}

67
vendor/league/csv/src/MapIterator.php vendored Normal file
View File

@ -0,0 +1,67 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use ArrayIterator;
use Iterator;
use IteratorAggregate;
use IteratorIterator;
use Traversable;
/**
* Maps value from an iterator before yielding.
*
* @internal used internally to modify CSV content
*/
final class MapIterator extends IteratorIterator
{
/** @var callable The callback to apply on all InnerIterator current value. */
private $callable;
public function __construct(Traversable $iterator, callable $callable)
{
parent::__construct($iterator);
$this->callable = $callable;
}
public static function fromIterable(iterable $iterator, callable $callable): self
{
return match (true) {
$iterator instanceof Traversable => new self($iterator, $callable),
is_array($iterator) => new self(new ArrayIterator($iterator), $callable),
};
}
public function current(): mixed
{
return ($this->callable)(parent::current(), parent::key());
}
public static function toIterator(iterable $value): Iterator
{
if ($value instanceof IteratorAggregate) {
$value = $value->getIterator();
}
return match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => (function () use ($value): Iterator {
foreach ($value as $offset => $record) {
yield $offset => $record;
}
})(),
default => new ArrayIterator($value),
};
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use CallbackFilterIterator;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use League\Csv\Query;
use ReflectionException;
/**
* Enable filtering a record based on the value of a one of its cell.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record offset WILL NOT BE taken into account
*/
final class Column implements Query\Predicate
{
/**
* @throws Query\QueryException
*/
private function __construct(
public readonly string|int $column,
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}
/**
* @throws Query\QueryException
*/
public static function filterOn(
string|int $column,
Comparison|Closure|callable|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($column, $operator, null);
}
if (is_callable($operator)) {
return new self($column, $operator(...), $value);
}
return new self(
$column,
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}
/**
* @throws ReflectionException
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
$subject = Query\Row::from($value)->value($this->column);
if ($this->operator instanceof Closure) {
return ($this->operator)($subject);
}
return $this->operator->compare($subject, $this->value);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(MapIterator::toIterator($value), $this);
}
}

View File

@ -0,0 +1,157 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use League\Csv\Query\QueryException;
use function array_is_list;
use function count;
use function in_array;
use function is_array;
use function is_scalar;
use function is_string;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strtoupper;
use function trim;
enum Comparison: string
{
case Equals = '=';
case NotEquals = '!=';
case GreaterThan = '>';
case GreaterThanOrEqual = '>=';
case LesserThan = '<';
case LesserThanOrEqual = '<=';
case Between = 'BETWEEN';
case NotBetween = 'NBETWEEN';
case Regexp = 'REGEXP';
case NotRegexp = 'NREGEXP';
case In = 'IN';
case NotIn = 'NIN';
case Contains = 'CONTAINS';
case NotContain = 'NCONTAIN';
case StartsWith = 'STARTS_WITH';
case EndsWith = 'ENDS_WITH';
public static function tryFromOperator(string $operator): ?self
{
$operator = strtoupper(trim($operator));
return match ($operator) {
'<>', 'NEQ', 'IS NOT', 'NOT EQUAL' => self::NotEquals,
'EQ', 'IS', 'EQUAL', 'EQUALS' => self::Equals,
'GT', 'GREATER THAN' => self::GreaterThan,
'GTE', 'GREATER THAN OR EQUAL' => self::GreaterThanOrEqual,
'LT', 'LESSER THAN' => self::LesserThan,
'LTE', 'LESSER THAN OR EQUAL' => self::LesserThanOrEqual,
'NOT_REGEXP', 'NOT REGEXP' => self::NotRegexp,
'NOT_CONTAIN', 'NOT CONTAIN', 'DOES_NOT_CONTAIN', 'DOES NOT CONTAIN' => self::NotContain,
'NOT_IN', 'NOT IN' => self::NotIn,
'NOT_BETWEEN', 'NOT BETWEEN' => self::Between,
'STARTS WITH', 'START WITH' => self::StartsWith,
'ENDS WITH', 'END WITH' => self::EndsWith,
default => self::tryFrom($operator),
};
}
/**
* @throws QueryException
*/
public static function fromOperator(string $operator): self
{
return self::tryFromOperator($operator) ?? throw QueryException::dueToUnknownOperator($operator);
}
/**
* Values comparison.
*
* The method return true if the values satisfy the comparison operator, otherwise false is returned.
*
* @throws QueryException
*/
public function compare(mixed $subject, mixed $reference): bool
{
$this->accept($reference);
return match ($this) {
self::Equals => self::isSingleValue($subject) ? $subject === $reference : $subject == $reference,
self::NotEquals => self::isSingleValue($subject) ? $subject !== $reference : $subject != $reference,
self::GreaterThan => $subject > $reference,
self::GreaterThanOrEqual => $subject >= $reference,
self::LesserThan => $subject < $reference,
self::LesserThanOrEqual => $subject <= $reference,
self::Between => $subject >= $reference[0] && $subject <= $reference[1], /* @phpstan-ignore-line */
self::NotBetween => $subject < $reference[0] || $subject > $reference[1], /* @phpstan-ignore-line */
self::In => in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::NotIn => !in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::Regexp => is_string($subject) && 1 === preg_match($reference, $subject), /* @phpstan-ignore-line */
self::NotRegexp => is_string($subject) && 1 !== preg_match($reference, $subject), /* @phpstan-ignore-line */
self::Contains => is_string($subject) && str_contains($subject, $reference), /* @phpstan-ignore-line */
self::NotContain => is_string($subject) && !str_contains($subject, $reference), /* @phpstan-ignore-line */
self::StartsWith => is_string($subject) && str_starts_with($subject, $reference), /* @phpstan-ignore-line */
self::EndsWith => is_string($subject) && str_ends_with($subject, $reference), /* @phpstan-ignore-line */
};
}
private static function isSingleValue(mixed $value): bool
{
return is_scalar($value) || null === $value;
}
/**
* Assert if the reference value can be used with the Enum operator.
*
* @throws QueryException
*/
public function accept(mixed $reference): void
{
match ($this) {
self::Between,
self::NotBetween => match (true) {
!is_array($reference),
!array_is_list($reference),
2 !== count($reference) => throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an list containing 2 values, the minimum and maximum values.'),
default => true,
},
self::In,
self::NotIn => match (true) {
!is_array($reference) => throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an array.'),
default => true,
},
self::Regexp,
self::NotRegexp => match (true) {
!is_string($reference),
'' === $reference,
false === @preg_match($reference, '') => throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a valid regular expression pattern string.'),
default => true,
},
self::Contains,
self::NotContain,
self::StartsWith,
self::EndsWith => match (true) {
!is_string($reference),
'' === $reference => throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a non empty string.'),
default => true,
},
self::Equals,
self::NotEquals,
self::GreaterThanOrEqual,
self::GreaterThan,
self::LesserThanOrEqual,
self::LesserThan => true,
};
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use CallbackFilterIterator;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use League\Csv\Query\Predicate;
use League\Csv\Query\PredicateCombinator;
use function array_reduce;
/**
* @phpstan-import-type Condition from PredicateCombinator
* @phpstan-import-type ConditionExtended from PredicateCombinator
*/
final class Criteria implements PredicateCombinator
{
/**
* @param Condition $predicate
*/
private function __construct(private readonly Predicate|Closure $predicate)
{
}
/**
* Creates a new instance with predicates join using the logical AND operator.
*
* @param ConditionExtended ...$predicates
*/
public static function all(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if (!$predicate($value, $key)) {
return false;
}
}
return true;
});
}
/**
* Creates a new instance with predicates join using the logical NOT operator.
*
* @param ConditionExtended ...$predicates
*/
public static function none(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate($value, $key)) {
return false;
}
}
return true;
});
}
/**
* Creates a new instance with predicates join using the logical OR operator.
*
* @param ConditionExtended ...$predicates
*/
public static function any(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate($value, $key)) {
return true;
}
}
return false;
});
}
/**
* Creates a new instance with predicates join using the logical XOR operator.
*
* @param ConditionExtended ...$predicates
*/
public static function xany(Predicate|Closure|callable ...$predicates): self
{
return new self(fn (mixed $value, int|string $key): bool => array_reduce(
$predicates,
fn (bool $bool, Predicate|Closure|callable $predicate) => $predicate($value, $key) xor $bool,
false
));
}
public function __invoke(mixed $value, int|string $key): bool
{
return ($this->predicate)($value, $key);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(MapIterator::toIterator($value), $this);
}
/**
* @param ConditionExtended ...$predicates
*/
public function and(Predicate|Closure|callable ...$predicates): self
{
return self::all($this->predicate, ...$predicates);
}
public function andNot(Predicate|Closure|callable ...$predicates): self
{
return self::all($this->predicate, self::none(...$predicates));
}
/**
* @param ConditionExtended ...$predicates
*/
public function not(Predicate|Closure|callable ...$predicates): self
{
return self::none($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function or(Predicate|Closure|callable ...$predicates): self
{
return self::any($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function orNot(Predicate|Closure|callable ...$predicates): self
{
return self::any($this->predicate, self::none(...$predicates));
}
/**
* @param ConditionExtended ...$predicates
*/
public function xor(Predicate|Closure|callable ...$predicates): self
{
return self::xany($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function xorNot(Predicate|Closure|callable ...$predicates): self
{
return self::xany($this->predicate, self::none(...$predicates));
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use CallbackFilterIterator;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use League\Csv\Query;
/**
* Enable filtering a record based on its offset.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record value WILL NOT BE taken into account
*/
final class Offset implements Query\Predicate
{
/**
* @throws Query\QueryException
*/
private function __construct(
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}
/**
* @throws Query\QueryException
*/
public static function filterOn(
Comparison|Closure|callable|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($operator, null);
}
if (is_callable($operator)) {
return new self(Closure::fromCallable($operator), $value);
}
return new self(
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}
/**
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
if ($this->operator instanceof Closure) {
return ($this->operator)($key);
}
return $this->operator->compare($key, $this->value);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(MapIterator::toIterator($value), $this);
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use CallbackFilterIterator;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use League\Csv\Query\Predicate;
use League\Csv\Query\QueryException;
use League\Csv\Query\Row;
use ReflectionException;
use function array_filter;
use function is_array;
use function is_int;
use function is_string;
use const ARRAY_FILTER_USE_BOTH;
/**
* Enable filtering a record by comparing the values of two of its column.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record offset WILL NOT BE taken into account
*/
final class TwoColumns implements Predicate
{
/**
* @throws QueryException
*/
private function __construct(
public readonly string|int $first,
public readonly Comparison|Closure $operator,
public readonly array|string|int $second,
) {
!$this->operator instanceof Closure || !is_array($this->second) || throw new QueryException('The second column must be a string if the operator is a callback.');
if (is_array($this->second)) {
$res = array_filter($this->second, fn (mixed $value): bool => !is_string($value) && !is_int($value));
if ([] !== $res) {
throw new QueryException('The second column must be a string, an integer or a list of strings and/or integer when the operator is not a callback.');
}
}
}
/**
* @throws QueryException
*/
public static function filterOn(
string|int $firstColumn,
Comparison|Closure|callable|string $operator,
array|string|int $secondColumn
): self {
if (is_string($operator)) {
$operator = Comparison::fromOperator($operator);
}
if (is_callable($operator)) {
return new self($firstColumn, Closure::fromCallable($operator), $secondColumn);
}
return new self($firstColumn, $operator, $secondColumn);
}
/**
* @throws QueryException
* @throws ReflectionException
*/
public function __invoke(mixed $value, int|string $key): bool
{
$val = match (true) {
is_array($this->second) => array_values(Row::from($value)->select(...$this->second)),
default => Row::from($value)->value($this->second),
};
if ($this->operator instanceof Closure) {
return ($this->operator)(Row::from($value)->value($this->first), $val);
}
return Column::filterOn($this->first, $this->operator, $val)($value, $key);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(MapIterator::toIterator($value), $this);
}
}

41
vendor/league/csv/src/Query/Limit.php vendored Normal file
View File

@ -0,0 +1,41 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use League\Csv\MapIterator;
use LimitIterator;
final class Limit
{
private function __construct(
public readonly int $offset,
public readonly int $length,
) {
0 <= $this->offset || throw new QueryException(self::class.' expects the offset to be greater or equal to 0, '.$this->offset.' given.');
-2 < $this->length || throw new QueryException(self::class.' expects the length to be greater or equal to -1, '.$this->length.' given.');
}
public static function new(int $offset, int $length): self
{
return new self($offset, $length);
}
/**
* Allows iteration over a limited subset of items in an iterable structure.
*/
public function slice(iterable $value): LimitIterator
{
return new LimitIterator(MapIterator::toIterator($value), $this->offset, $this->length);
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Ordering;
use ArrayIterator;
use Closure;
use Iterator;
use League\Csv\Query\QueryException;
use League\Csv\Query\Row;
use League\Csv\Query\Sort;
use OutOfBoundsException;
use ReflectionException;
use function is_array;
use function is_string;
use function iterator_to_array;
use function strtoupper;
use function trim;
/**
* Enable sorting a record based on the value of a one of its cell.
*/
final class Column implements Sort
{
private const ASCENDING = 'ASC';
private const DESCENDING = 'DESC';
/**
* @param Closure(mixed, mixed): int $callback
*/
private function __construct(
public readonly string $direction,
public readonly string|int $column,
public readonly Closure $callback,
) {
}
/**
* @param (callable(mixed, mixed): int)|(Closure(mixed, mixed): int)|null $callback
*
* @throws QueryException
*/
public static function sortOn(
string|int $column,
string|int $direction,
Closure|callable|null $callback = null
): self {
$operator = match (true) {
SORT_ASC === $direction => self::ASCENDING,
SORT_DESC === $direction => self::DESCENDING,
is_string($direction) => match (strtoupper(trim($direction))) {
'ASC', 'ASCENDING', 'UP' => self::ASCENDING,
'DESC', 'DESCENDING', 'DOWN' => self::DESCENDING,
default => throw new QueryException('Unknown or unsupported ordering operator value: '.$direction),
},
default => throw new QueryException('Unknown or unsupported ordering operator value: '.$direction),
};
$callback = match (true) {
null === $callback => static fn (mixed $first, mixed $second): int => $first <=> $second,
$callback instanceof Closure => $callback,
default => $callback(...),
};
return new self($operator, $column, $callback);
}
/**
* @throws ReflectionException
* @throws QueryException
*/
public function __invoke(mixed $valueA, mixed $valueB): int
{
$first = Row::from($valueA)->value($this->column);
$second = Row::from($valueB)->value($this->column);
return match ($this->direction) {
self::ASCENDING => ($this->callback)($first, $second),
default => ($this->callback)($second, $first),
};
}
public function sort(iterable $value): Iterator
{
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
$it = new $class(!is_array($value) ? iterator_to_array($value) : $value);
$it->uasort($this);
return $it;
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query\Ordering;
use ArrayIterator;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use League\Csv\Query\Sort;
use League\Csv\Query\SortCombinator;
use OutOfBoundsException;
use function array_map;
/**
* Enable sorting a record based on multiple column.
*
* The class can be used with PHP's usort and uasort functions.
*
* @phpstan-import-type Ordering from SortCombinator
* @phpstan-import-type OrderingExtended from SortCombinator
*/
final class MultiSort implements SortCombinator
{
/** @var array<Ordering> */
private readonly array $sorts;
/**
* @param OrderingExtended ...$sorts
*/
private function __construct(Sort|Closure|callable ...$sorts)
{
$this->sorts = array_map(
static fn (Sort|Closure|callable $sort): Sort|Closure => $sort instanceof Closure || $sort instanceof Sort ? $sort : $sort(...),
$sorts
);
}
/**
* @param OrderingExtended ...$sorts
*/
public static function all(Sort|Closure|callable ...$sorts): self
{
return new self(...$sorts);
}
/**
* @param OrderingExtended ...$sorts
*/
public function append(Sort|Closure|callable ...$sorts): self
{
if ([] === $sorts) {
return $this;
}
return new self(...$this->sorts, ...$sorts);
}
/**
* @param OrderingExtended ...$sorts
*/
public function prepend(Sort|Closure|callable ...$sorts): self
{
if ([] === $sorts) {
return $this;
}
return (new self(...$sorts))->append(...$this->sorts);
}
public function __invoke(mixed $valueA, mixed $valueB): int
{
foreach ($this->sorts as $sort) {
if (0 !== ($result = $sort($valueA, $valueB))) {
return $result;
}
}
return $result ?? 0;
}
public function sort(iterable $value): Iterator
{
if ([] === $this->sorts) {
return MapIterator::toIterator($value);
}
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
$it = new $class(!is_array($value) ? iterator_to_array($value) : $value);
$it->uasort($this);
return $it;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use Iterator;
/**
* Enable filtering a record based on its value and/or its offset.
*
* The class can be used directly with PHP's
* <ol>
* <li>array_filter with the ARRAY_FILTER_USE_BOTH flag.</li>
* <li>CallbackFilterIterator class.</li>
* </ol>
*/
interface Predicate
{
/**
* The class predicate method.
*
* Evaluates each element of an iterable structure based on its value and its offset.
* The method must return true if the predicate is satisfied, false otherwise.
*/
public function __invoke(mixed $value, string|int $key): bool;
/**
* Filters elements of an iterable structure using the class predicate method.
*
* @see Predicate::__invoke
*/
public function filter(iterable $value): Iterator;
}

View File

@ -0,0 +1,71 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use Closure;
/**
* @phpstan-type Condition Predicate|Closure(mixed, array-key): bool
* @phpstan-type ConditionExtended Predicate|Closure(mixed, array-key): bool|callable(mixed, array-key): bool
*/
interface PredicateCombinator extends Predicate
{
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the AND Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function and(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the OR Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function or(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the NOT Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function not(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the XOR Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function xor(Predicate|Closure ...$predicates): self;
}

View File

@ -0,0 +1,44 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use Exception;
use League\Csv\UnableToProcessCsv;
final class QueryException extends Exception implements UnableToProcessCsv
{
public static function dueToUnknownColumn(string|int $column, array|object $value): self
{
return match (true) {
is_object($value) => match (is_int($column)) {
true => new self('The object property name can not be the integer`'.$column.'`.'),
default => new self('The object property name `'.$column.'` could not be retrieved from the object.'),
},
default => match (is_string($column)) {
true => new self('The column `'.$column.'` does not exist in the input array.'),
default => new self('The column with the offset `'.$column.'` does not exist in the input array.'),
},
};
}
public static function dueToMissingColumn(): self
{
return new self('No valid column were found with the given data.');
}
public static function dueToUnknownOperator(string $operator): self
{
return new self('Unknown or unsupported comparison operator `'.$operator.'`');
}
}

176
vendor/league/csv/src/Query/Row.php vendored Normal file
View File

@ -0,0 +1,176 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use ArrayAccess;
use ReflectionException;
use ReflectionObject;
use TypeError;
use function array_is_list;
use function array_key_exists;
use function array_map;
use function array_values;
use function count;
use function explode;
use function implode;
use function is_array;
use function is_int;
use function is_object;
use function lcfirst;
use function str_replace;
final class Row
{
public static function from(mixed $value): Row
{
return new self(match (true) {
is_object($value),
is_array($value) => $value,
default => throw new TypeError('The value must be an array or an object; received '.gettype($value).'.'),
});
}
private function __construct(private readonly array|object $row)
{
}
/**
* Tries to retrieve a single value from a record.
*
* @throws ReflectionException
* @throws QueryException If the value can not be retrieved
* @see Row::select()
*
*/
public function value(string|int $key): mixed
{
return $this->select($key)[$key];
}
/**
* Tries to retrieve multiple values from a record.
*
* If the value is an array and the key is an integer the content will be retrieved
* from the array_values array form. Negative offset are supported.
* If the value is an object, the key MUST be a string.
*
* @throws ReflectionException
* @throws QueryException If the value can not be retrieved
*
* @return non-empty-array<array-key, mixed>
*/
public function select(string|int ...$key): array
{
return match (true) {
is_object($this->row) => self::getObjectPropertyValue($this->row, ...$key),
default => self::getArrayEntry($this->row, ...$key),
};
}
/**
* @throws QueryException
*
* @return non-empty-array<array-key, mixed>
*/
private function getArrayEntry(array $row, string|int ...$keys): array
{
$res = [];
$arrValues = array_values($row);
foreach ($keys as $key) {
if (array_key_exists($key, $res)) {
continue;
}
$offset = $key;
if (is_int($offset)) {
if (!array_is_list($row)) {
$row = $arrValues;
}
if ($offset < 0) {
$offset += count($row);
}
}
$res[$key] = array_key_exists($offset, $row) ? $row[$offset] : throw QueryException::dueToUnknownColumn($key, $row);
}
return [] !== $res ? $res : throw QueryException::dueToMissingColumn();
}
/**
* @throws ReflectionException
* @throws QueryException
*
* @return non-empty-array<array-key, mixed>
*/
private static function getObjectPropertyValue(object $row, string|int ...$keys): array
{
$res = [];
$object = new ReflectionObject($row);
foreach ($keys as $key) {
if (array_key_exists($key, $res)) {
continue;
}
!is_int($key) || throw QueryException::dueToUnknownColumn($key, $row);
if ($object->hasProperty($key) && $object->getProperty($key)->isPublic()) {
$res[$key] = $object->getProperty($key)->getValue($row);
continue;
}
$methodNameList = [$key];
if (($camelCasedKey = self::camelCase($key)) !== $key) {
$methodNameList[] = $camelCasedKey;
}
$methodNameList[] = self::camelCase($key, 'get');
foreach ($methodNameList as $methodName) {
if ($object->hasMethod($methodName)
&& $object->getMethod($methodName)->isPublic()
&& 1 > $object->getMethod($methodName)->getNumberOfRequiredParameters()
) {
$res[$key] = $object->getMethod($methodName)->invoke($row);
continue 2;
}
}
if (method_exists($row, '__call')) {
$res[$key] = $object->getMethod('__call')->invoke($row, $methodNameList[1]);
continue;
}
if ($row instanceof ArrayAccess && $row->offsetExists($key)) {
$res[$key] = $row->offsetGet($key);
continue;
}
throw QueryException::dueToUnknownColumn($key, $row);
}
return [] !== $res ? $res : throw QueryException::dueToMissingColumn();
}
private static function camelCase(string $value, string $prefix = ''): string
{
if ('' !== $prefix) {
$prefix .= '_';
}
return lcfirst(implode('', array_map(
ucfirst(...),
explode(' ', str_replace(['-', '_'], ' ', $prefix.$value))
)));
}
}

43
vendor/league/csv/src/Query/Sort.php vendored Normal file
View File

@ -0,0 +1,43 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use Iterator;
/**
* Enable sorting a record based on its value.
*
* The class can be used directly with PHP's
* <ol>
* <li>usort and uasort.</li>
* <li>ArrayIterator::uasort.</li>
* <li>ArrayObject::uasort.</li>
* </ol>
*/
interface Sort
{
/**
* The class comparison method.
*
* The method must return an integer less than, equal to, or greater than zero
* if the first argument is considered to be respectively less than, equal to,
* or greater than the second.
*/
public function __invoke(mixed $valueA, mixed $valueB): int;
/**
* Sort an iterable structure with the class comparison method and maintain index association.
*/
public function sort(iterable $value): Iterator;
}

View File

@ -0,0 +1,45 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Query;
use Closure;
/**
* @phpstan-type Ordering Sort|Closure(mixed, mixed): int
* @phpstan-type OrderingExtended Sort|Closure(mixed, mixed): int|callable(mixed, mixed): int
*/
interface SortCombinator extends Sort
{
/**
* Return an instance with the specified sorting algorithm
* added after the currently registered sorting algorithms.
*
* This method MUST retain the state of the current instance,
* and return an instance that contains the specified changes.
*
* @param Ordering ...$sorts
*/
public function append(Sort|Closure ...$sorts): self;
/**
* Return an instance with the specified sorting algorithm
* added before the currently registered sorting algorithms.
*
* This method MUST retain the state of the current instance,
* and return an instance that contains the specified changes.
*
* @param Ordering ...$sorts
*/
public function prepend(Sort|Closure ...$sorts): self;
}

216
vendor/league/csv/src/RFC4180Field.php vendored Normal file
View File

@ -0,0 +1,216 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use InvalidArgumentException;
use php_user_filter;
use Throwable;
use TypeError;
use function array_map;
use function in_array;
use function is_string;
use function restore_error_handler;
use function set_error_handler;
use function str_replace;
use function strcspn;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_bucket_new;
use function stream_filter_register;
use function stream_get_filters;
use function strlen;
use function trigger_error;
use const E_USER_WARNING;
use const PSFS_ERR_FATAL;
use const PSFS_PASS_ON;
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
/**
* A stream filter to conform the CSV field to RFC4180.
*
* DEPRECATION WARNING! This class will be removed in the next major point release
*
* @deprecated since version 9.2.0
* @see AbstractCsv::setEscape
*
* @see https://tools.ietf.org/html/rfc4180#section-2
*/
class RFC4180Field extends php_user_filter
{
#[Deprecated(message: 'use League\Csv\Reader::setEscape or League\Csv\Writer::setEscape instead', since: 'league/csv:9.2.0')]
public const FILTERNAME = 'convert.league.csv.rfc4180';
/**
* The value being search for.
*
* @var array<string>
*/
protected array $search = [];
/**
* The replacement value that replace found $search values.
*
* @var array<string>
*/
protected array $replace = [];
/**
* Characters that triggers enclosure with PHP fputcsv.
*/
protected static string $force_enclosure = "\n\r\t ";
/**
* Static method to add the stream filter to a {@link AbstractCsv} object.
*/
public static function addTo(AbstractCsv $csv, string $whitespace_replace = ''): AbstractCsv
{
self::register();
$params = [
'enclosure' => $csv->getEnclosure(),
'escape' => $csv->getEscape(),
'mode' => $csv instanceof Writer ? STREAM_FILTER_WRITE : STREAM_FILTER_READ,
];
if ($csv instanceof Writer && '' !== $whitespace_replace) {
self::addFormatterTo($csv, $whitespace_replace);
$params['whitespace_replace'] = $whitespace_replace;
}
return $csv->addStreamFilter(self::FILTERNAME, $params);
}
/**
* Add a formatter to the {@link Writer} object to format the record
* field to avoid enclosure around a field with an empty space.
*/
#[Deprecated(message: 'use League\Csv\Reader::setEscape or League\Csv\Writer::setEscape instead', since: 'league/csv:9.2.0')]
public static function addFormatterTo(Writer $csv, string $whitespace_replace): Writer
{
if ('' == $whitespace_replace || strlen($whitespace_replace) !== strcspn($whitespace_replace, self::$force_enclosure)) {
throw new InvalidArgumentException('The sequence contains a character that enforces enclosure or is a CSV control character or is an empty string.');
}
$mapper = fn ($value) => is_string($value)
? str_replace(' ', $whitespace_replace, $value)
: $value;
return $csv->addFormatter(fn (array $record): array => array_map($mapper, $record));
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
if (!in_array(self::FILTERNAME, stream_get_filters(), true)) {
stream_filter_register(self::FILTERNAME, self::class);
}
}
/**
* Static method to return the stream filter filtername.
*/
public static function getFiltername(): string
{
return self::FILTERNAME;
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
*/
public function filter($in, $out, &$consumed, bool $closing): int
{
$data = '';
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$data .= $bucket->data;
$consumed += $bucket->datalen;
}
try {
$data = str_replace($this->search, $this->replace, $data);
} catch (Throwable $exception) {
trigger_error('An error occurred while executing the stream filter `'.$this->filtername.'`: '.$exception->getMessage(), E_USER_WARNING);
return PSFS_ERR_FATAL;
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
stream_bucket_append($out, stream_bucket_new($this->stream, $data));
restore_error_handler();
return PSFS_PASS_ON;
}
#[Deprecated(message: 'use League\Csv\Reader::setEscape or League\Csv\Writer::setEscape instead', since: 'league/csv:9.2.0')]
public function onCreate(): bool
{
if (!is_array($this->params)) {
throw new TypeError('The filter parameters must be an array.');
}
static $mode_list = [STREAM_FILTER_READ => 1, STREAM_FILTER_WRITE => 1];
$state = isset($this->params['enclosure'], $this->params['escape'], $this->params['mode'], $mode_list[$this->params['mode']])
&& 1 === strlen($this->params['enclosure'])
&& 1 === strlen($this->params['escape']);
if (false === $state) {
return false;
}
$this->search = [$this->params['escape'].$this->params['enclosure']];
$this->replace = [$this->params['enclosure'].$this->params['enclosure']];
if (STREAM_FILTER_WRITE !== $this->params['mode']) {
return true;
}
$this->search = [$this->params['escape'].$this->params['enclosure']];
$this->replace = [$this->params['escape'].$this->params['enclosure'].$this->params['enclosure']];
if ($this->isValidSequence($this->params)) {
$this->search[] = $this->params['whitespace_replace'];
$this->replace[] = ' ';
}
return true;
}
/**
* @codeCoverageIgnore
* Validate params property.
*/
protected function isValidParams(array $params): bool
{
static $mode_list = [STREAM_FILTER_READ => 1, STREAM_FILTER_WRITE => 1];
return isset($params['enclosure'], $params['escape'], $params['mode'], $mode_list[$params['mode']])
&& 1 === strlen($params['enclosure'])
&& 1 === strlen($params['escape']);
}
/**
* Is Valid White space replaced sequence.
*/
protected function isValidSequence(array $params): bool
{
return isset($params['whitespace_replace'])
&& strlen($params['whitespace_replace']) === strcspn($params['whitespace_replace'], self::$force_enclosure);
}
}

83
vendor/league/csv/src/RdbmsResult.php vendored Normal file
View File

@ -0,0 +1,83 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Generator;
use mysqli_result;
use PDO;
use PDOStatement;
use PgSql\Result;
use RuntimeException;
use SQLite3Result;
use function array_column;
use function array_map;
use function pg_fetch_assoc;
use function pg_field_name;
use function pg_num_fields;
use function range;
use const SQLITE3_ASSOC;
final class RdbmsResult
{
/**
* @throws RuntimeException If no column names information is found.
*
* @return list<string>
*/
public static function columnNames(PDOStatement|Result|mysqli_result|SQLite3Result $result): array
{
return match (true) {
$result instanceof PDOStatement => array_map(fn (int $index): string => $result->getColumnMeta($index)['name'] ?? throw new RuntimeException('Unable to get metadata for column '.$index), range(0, $result->columnCount() - 1)),
$result instanceof mysqli_result => array_column($result->fetch_fields(), 'name'),
$result instanceof Result => array_map(fn (int $index) => pg_field_name($result, $index), range(0, pg_num_fields($result) - 1)),
$result instanceof SQLite3Result => array_map($result->columnName(...), range(0, $result->numColumns() - 1)),
};
}
/**
* @return Generator<int, array<array-key, mixed>>
*/
public static function rows(PDOStatement|Result|mysqli_result|SQLite3Result $result): Generator
{
if ($result instanceof PDOStatement) {
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
yield $row; /* @phpstan-ignore-line */
}
return;
}
if ($result instanceof Result) {
while ($row = pg_fetch_assoc($result)) {
yield $row;
}
return;
}
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
yield $row;
}
return;
}
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
yield $row;
}
}
}

666
vendor/league/csv/src/Reader.php vendored Normal file
View File

@ -0,0 +1,666 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use CallbackFilterIterator;
use Closure;
use Deprecated;
use Iterator;
use JsonSerializable;
use League\Csv\Serializer\Denormalizer;
use League\Csv\Serializer\MappingFailed;
use League\Csv\Serializer\TypeCastingFailed;
use SplFileObject;
use function array_filter;
use function array_unique;
use function is_array;
use function iterator_count;
use function strlen;
use function substr;
use const STREAM_FILTER_READ;
/**
* A class to parse and read records from a CSV document.
*
* @template TValue of array
*/
class Reader extends AbstractCsv implements TabularDataReader, JsonSerializable
{
protected const STREAM_FILTER_MODE = STREAM_FILTER_READ;
protected ?int $header_offset = null;
protected int $nb_records = -1;
protected bool $is_empty_records_included = false;
/** @var array<string> header record. */
protected array $header = [];
public static function createFromPath(string $path, string $open_mode = 'r', $context = null): static
{
return parent::createFromPath($path, $open_mode, $context);
}
/**
* Selects the record to be used as the CSV header.
*
* Because the header is represented as an array, to be valid
* a header MUST contain only unique string value.
*
* @param int|null $offset the header record offset
*
* @throws Exception if the offset is a negative integer
*/
public function setHeaderOffset(?int $offset): static
{
if ($offset === $this->header_offset) {
return $this;
}
null === $offset || -1 < $offset || throw InvalidArgument::dueToInvalidHeaderOffset($offset, __METHOD__);
$this->header_offset = $offset;
$this->resetProperties();
return $this;
}
/**
* Enables skipping empty records.
*/
public function skipEmptyRecords(): static
{
if ($this->is_empty_records_included) {
$this->is_empty_records_included = false;
$this->nb_records = -1;
}
return $this;
}
/**
* Disables skipping empty records.
*/
public function includeEmptyRecords(): static
{
if (!$this->is_empty_records_included) {
$this->is_empty_records_included = true;
$this->nb_records = -1;
}
return $this;
}
/**
* Tells whether empty records are skipped by the instance.
*/
public function isEmptyRecordsIncluded(): bool
{
return $this->is_empty_records_included;
}
protected function resetProperties(): void
{
parent::resetProperties();
$this->nb_records = -1;
$this->header = [];
}
/**
* Returns the header offset.
*/
public function getHeaderOffset(): ?int
{
return $this->header_offset;
}
/**
* @throws SyntaxError
*
* Returns the header record.
*/
public function getHeader(): array
{
return match (true) {
null === $this->header_offset,
[] !== $this->header => $this->header,
default => ($this->header = $this->setHeader($this->header_offset)),
};
}
/**
* Determines the CSV record header.
*
* @throws SyntaxError If the header offset is set and no record is found or is the empty array
*
* @return array<string>
*/
protected function setHeader(int $offset): array
{
$inputBom = null;
$header = $this->seekRow($offset);
if (0 === $offset) {
$inputBom = Bom::tryFrom($this->getInputBOM());
$header = $this->removeBOM(
$header,
!$this->is_input_bom_included ? $inputBom?->length() ?? 0 : 0,
$this->enclosure
);
}
return match (true) {
[] === $header,
[null] === $header,
[false] === $header,
[''] === $header && 0 === $offset && null !== $inputBom => throw SyntaxError::dueToHeaderNotFound($offset),
default => $header,
};
}
/**
* @throws Exception
*
* Returns the row at a given offset.
*/
protected function seekRow(int $offset): array
{
$this->getDocument()->seek($offset);
$record = $this->document->current();
return match (true) {
false === $record => [],
default => (array) $record,
};
}
/**
* @throws Exception
*
* Returns the document as an Iterator.
*/
protected function getDocument(): SplFileObject|Stream
{
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->rewind();
return $this->document;
}
/**
* Strips the BOM sequence from a record.
*
* @param array<string> $record
*
* @return array<string>
*/
protected function removeBOM(array $record, int $bom_length, string $enclosure): array
{
if ([] === $record || !is_string($record[0]) || 0 === $bom_length || strlen($record[0]) < $bom_length) {
return $record;
}
$record[0] = substr($record[0], $bom_length);
if ($enclosure.$enclosure !== substr($record[0].$record[0], strlen($record[0]) - 1, 2)) {
return $record;
}
$record[0] = substr($record[0], 1, -1);
return $record;
}
public function fetchColumn(string|int $index = 0): Iterator
{
return ResultSet::from($this)->fetchColumn($index);
}
public function value(int|string $column = 0): mixed
{
return ResultSet::from($this)->value($column);
}
/**
* @throws Exception
*/
public function first(): array
{
return ResultSet::from($this)->first();
}
/**
* @throws Exception
*/
public function nth(int $nth): array
{
return ResultSet::from($this)->nth($nth);
}
/**
* @param class-string $className
*
* @throws Exception
*/
public function nthAsObject(int $nth, string $className, array $header = []): ?object
{
return ResultSet::from($this)->nthAsObject($nth, $className, $header);
}
/**
* @param class-string $className
*
* @throws Exception
*/
public function firstAsObject(string $className, array $header = []): ?object
{
return ResultSet::from($this)->firstAsObject($className, $header);
}
public function fetchPairs(string|int $offset_index = 0, string|int $value_index = 1): Iterator
{
return ResultSet::from($this)->fetchPairs($offset_index, $value_index);
}
/**
* @throws Exception
*/
public function count(): int
{
if (-1 === $this->nb_records) {
$this->nb_records = iterator_count($this->getRecords());
}
return $this->nb_records;
}
/**
* @throws Exception
*/
public function getIterator(): Iterator
{
return $this->getRecords();
}
/**
* @throws Exception
*/
public function jsonSerialize(): array
{
return array_values([...$this->getRecords()]);
}
/**
* @param callable(array<mixed>, array-key=): (void|bool|null) $callback
*/
public function each(callable $callback): bool
{
return ResultSet::from($this)->each($callback);
}
/**
* @param callable(array<mixed>, array-key=): bool $callback
*/
public function exists(callable $callback): bool
{
return ResultSet::from($this)->exists($callback);
}
/**
* @param callable(TInitial|null, array<mixed>, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @template TInitial
*
* @return TInitial|null
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
return ResultSet::from($this)->reduce($callback, $initial);
}
/**
* Run a map over each container members.
*
* @template TMap
*
* @param callable(array, int): TMap $callback
*
* @return Iterator<TMap>
*/
public function map(callable $callback): Iterator
{
return MapIterator::fromIterable($this, $callback);
}
/**
* @param positive-int $recordsCount
*
* @throws InvalidArgument
*
* @return iterable<TabularDataReader>
*/
public function chunkBy(int $recordsCount): iterable
{
return ResultSet::from($this)->chunkBy($recordsCount);
}
/**
* @param array<string> $headers
*/
public function mapHeader(array $headers): TabularDataReader
{
return (new Statement())->process($this, $headers);
}
/**
* @param \League\Csv\Query\Predicate|Closure(array, array-key): bool $predicate
*
* @throws Exception
* @throws SyntaxError
*/
public function filter(Query\Predicate|Closure $predicate): TabularDataReader
{
return (new Statement())->where($predicate)->process($this);
}
/**
* @param int<0, max> $offset
* @param int<-1, max> $length
*
* @throws Exception
* @throws SyntaxError
*/
public function slice(int $offset, int $length = -1): TabularDataReader
{
return (new Statement())->offset($offset)->limit($length)->process($this);
}
/**
* @param Closure(mixed, mixed): int $orderBy
*
* @throws Exception
* @throws SyntaxError
*/
public function sorted(Query\Sort|Closure $orderBy): TabularDataReader
{
return (new Statement())->orderBy($orderBy)->process($this);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract all found fragment identifiers for the specifield tabular data
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @return iterable<int, TabularDataReader>
*/
public function matching(string $expression): iterable
{
return (new FragmentFinder())->findAll($expression, $this);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or returns null
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
*/
public function matchingFirst(string $expression): ?TabularDataReader
{
return (new FragmentFinder())->findFirst($expression, $this);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or fail
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @throws FragmentNotFound
*/
public function matchingFirstOrFail(string $expression): TabularDataReader
{
return (new FragmentFinder())->findFirstOrFail($expression, $this);
}
public function select(string|int ...$columns): TabularDataReader
{
return ResultSet::from($this)->select(...$columns);
}
public function selectAllExcept(string|int ...$columns): TabularDataReader
{
return ResultSet::from($this)->selectAllExcept(...$columns);
}
/**
* @param array<string> $header
*
* @throws Exception
*
* @return Iterator<array-key, TValue>
*/
public function getRecords(array $header = []): Iterator
{
return $this->combineHeader(
$this->prepareRecords(),
$this->prepareHeader($header)
);
}
/**
* @template T of object
* @param class-string<T> $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*
* @return iterator<T>
*/
public function getRecordsAsObject(string $className, array $header = []): Iterator
{
/** @var array<string> $header */
$header = $this->prepareHeader($header);
return Denormalizer::assignAll(
$className,
$this->combineHeader($this->prepareRecords(), $header),
$header
);
}
/**
* @throws Exception
*/
protected function prepareRecords(): Iterator
{
$normalized = fn ($record): bool => is_array($record) && ($this->is_empty_records_included || $record !== [null]);
$bom = null;
if (!$this->is_input_bom_included) {
$bom = Bom::tryFrom($this->getInputBOM());
}
$records = $this->stripBOM(new CallbackFilterIterator($this->getDocument(), $normalized), $bom);
if (null !== $this->header_offset) {
$records = new CallbackFilterIterator($records, fn (array $record, int $offset): bool => $offset !== $this->header_offset);
}
if ($this->is_empty_records_included) {
$records = new MapIterator($records, fn (array $record): array => ([null] === $record) ? [] : $record);
}
return $records;
}
/**
* Strips the BOM sequence from the returned records if necessary.
*/
protected function stripBOM(Iterator $iterator, ?Bom $bom): Iterator
{
if (null === $bom) {
return $iterator;
}
$bomLength = $bom->length();
$mapper = function (array $record, int $index) use ($bomLength): array {
if (0 !== $index) {
return $record;
}
$record = $this->removeBOM($record, $bomLength, $this->enclosure);
return match ($record) {
[''] => [null],
default => $record,
};
};
return new CallbackFilterIterator(
new MapIterator($iterator, $mapper),
fn (array $record): bool => $this->is_empty_records_included || $record !== [null]
);
}
/**
* @param array<string> $header
*
* @throws SyntaxError
*
* @return array<int|string>
*/
protected function prepareHeader($header = []): array
{
$header == array_filter($header, is_string(...)) || throw SyntaxError::dueToInvalidHeaderColumnNames();
return $this->computeHeader($header);
}
/**
* Returns the header to be used for iteration.
*
* @param array<int|string> $header
*
* @throws SyntaxError If the header contains non unique column name
*
* @return array<int|string>
*/
protected function computeHeader(array $header): array
{
if ([] === $header) {
$header = $this->getHeader();
}
return match (true) {
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
[] !== array_filter(array_keys($header), fn (string|int $value) => !is_int($value) || $value < 0) => throw new SyntaxError('The header mapper indexes should only contain positive integer or 0.'),
default => $header,
};
}
protected function combineHeader(Iterator $iterator, array $header): Iterator
{
$formatter = fn (array $record): array => array_reduce(
$this->formatters,
fn (array $record, Closure $formatter): array => $formatter($record),
$record
);
return match ([]) {
$header => new MapIterator($iterator, $formatter(...)),
default => new MapIterator($iterator, function (array $record) use ($header, $formatter): array {
$assocRecord = [];
foreach ($header as $offset => $headerName) {
$assocRecord[$headerName] = $record[$offset] ?? null;
}
return $formatter($assocRecord);
}),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws Exception
*
* @deprecated since version 9.23.0
* @codeCoverageIgnore
*
* @see ResultSet::fetchColumn()
*/
#[Deprecated(message:'use League\Csv\Resultset::fetchColumn() instead', since:'league/csv:9.23.0')]
public function fetchColumnByName(string $name): Iterator
{
return ResultSet::from($this)->fetchColumnByName($name);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws Exception
*
* @deprecated since version 9.23.0
* @codeCoverageIgnore
*
* @see ResultSet::fetchColumn()
*/
#[Deprecated(message:'use League\Csv\Resultset::fetchColumn() instead', since:'league/csv:9.23.0')]
public function fetchColumnByOffset(int $offset = 0): Iterator
{
return ResultSet::from($this)->fetchColumnByOffset($offset);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Reader::nth()
* @deprecated since version 9.9.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\Reader::nth() instead', since:'league/csv:9.9.0')]
public function fetchOne(int $nth_record = 0): array
{
return $this->nth($nth_record);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Reader::getRecordsAsObject()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*
* @param class-string $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*/
#[Deprecated(message:'use League\Csv\Reader::getRecordsAsObject() instead', since:'league/csv:9.15.0')]
public function getObjects(string $className, array $header = []): Iterator
{
return $this->getRecordsAsObject($className, $header);
}
}

718
vendor/league/csv/src/ResultSet.php vendored Normal file
View File

@ -0,0 +1,718 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Deprecated;
use Generator;
use Iterator;
use JsonSerializable;
use League\Csv\Serializer\Denormalizer;
use League\Csv\Serializer\MappingFailed;
use League\Csv\Serializer\TypeCastingFailed;
use LimitIterator;
use mysqli_result;
use PDOStatement;
use PgSql\Result;
use RuntimeException;
use SQLite3Result;
use Throwable;
use function array_filter;
use function array_flip;
use function array_key_exists;
use function array_reduce;
use function array_search;
use function array_values;
use function is_int;
use function is_string;
use function iterator_count;
/**
* Represents the result set of a {@link Reader} processed by a {@link Statement}.
*
* @template TValue of array
*/
class ResultSet implements TabularDataReader, JsonSerializable
{
/** @var array<string> */
protected array $header;
/* @var Iterator<array-key, array<array-key, mixed>> */
protected Iterator $records;
/**
* @internal
*
* @see self::from() for public API usage
*
* @param Iterator|array<array-key, array<array-key, mixed>> $records
* @param array<string> $header
*
* @throws SyntaxError
*/
public function __construct(Iterator|array $records = [], array $header = [])
{
$header === array_filter($header, is_string(...)) || throw SyntaxError::dueToInvalidHeaderColumnNames();
$this->header = array_values($this->validateHeader($header));
$this->records = match (true) {
$records instanceof Iterator => $records,
default => new ArrayIterator($records),
};
}
/**
* @throws SyntaxError if the header syntax is invalid
*/
protected function validateHeader(array $header): array
{
return match (true) {
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
[] !== array_filter(array_keys($header), fn (string|int $value) => !is_int($value) || $value < 0) => throw new SyntaxError('The header mapper indexes should only contain positive integer or 0.'),
default => $header,
};
}
/**
* Returns a new instance from a tabular data implementing object.
*
* @throws RuntimeException|SyntaxError If the column names can not be found
*/
public static function tryFrom(PDOStatement|Result|mysqli_result|SQLite3Result|TabularData $tabularData): ?self
{
try {
return self::from($tabularData);
} catch (Throwable) {
return null;
}
}
/**
* Returns a new instance from a tabular data implementing object.
*
* @throws RuntimeException|SyntaxError If the column names can not be found
*/
public static function from(PDOStatement|Result|mysqli_result|SQLite3Result|TabularData $tabularData): self
{
if (!$tabularData instanceof TabularData) {
/** @var ArrayIterator<array-key, array<array-key, mixed>> $data */
$data = new ArrayIterator();
foreach (RdbmsResult::rows($tabularData) as $offset => $row) {
$data[$offset] = $row;
}
return new self($data, RdbmsResult::columnNames($tabularData));
}
return new self($tabularData->getRecords(), $tabularData->getHeader());
}
public function __destruct()
{
unset($this->records);
}
/**
* Returns the header associated with the result set.
*
* @return array<string>
*/
public function getHeader(): array
{
return $this->header;
}
/**
* @throws SyntaxError
*/
public function getIterator(): Iterator
{
return $this->getRecords();
}
/**
* @param callable(array<mixed>, array-key=): mixed $callback
*/
public function each(callable $callback): bool
{
foreach ($this as $offset => $record) {
if (false === $callback($record, $offset)) {
return false;
}
}
return true;
}
/**
* @param callable(array<mixed>, array-key=): bool $callback
*/
public function exists(callable $callback): bool
{
foreach ($this as $offset => $record) {
if (true === $callback($record, $offset)) {
return true;
}
}
return false;
}
/**
* @param callable(TInitial|null, array<mixed>, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @template TInitial
*
* @return TInitial|null
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
foreach ($this as $offset => $record) {
$initial = $callback($initial, $record, $offset);
}
return $initial;
}
/**
* Run a map over each container members.
*
* @template TMap
*
* @param callable(array, int): TMap $callback
*
* @return Iterator<TMap>
*/
public function map(callable $callback): Iterator
{
return MapIterator::fromIterable($this, $callback);
}
/**
* @param positive-int $recordsCount
*
* @throws InvalidArgument
*
* @return iterable<TabularDataReader>
*/
public function chunkBy(int $recordsCount): iterable
{
$recordsCount > 0 || throw InvalidArgument::dueToInvalidChunkSize($recordsCount, __METHOD__);
$header = $this->getHeader();
$records = [];
$nbRecords = 0;
foreach ($this->getRecords() as $record) {
$records[] = $record;
++$nbRecords;
if ($nbRecords === $recordsCount) {
yield new self($records, $header);
$records = [];
$nbRecords = 0;
}
}
if ([] !== $records) {
yield new self($records, $header);
}
}
/**
* @param array<string> $headers
*/
public function mapHeader(array $headers): TabularDataReader
{
return (new Statement())->process($this, $headers);
}
public function filter(Query\Predicate|Closure $predicate): TabularDataReader
{
return (new Statement())->where($predicate)->process($this);
}
public function slice(int $offset, ?int $length = null): TabularDataReader
{
return (new Statement())->offset($offset)->limit($length ?? -1)->process($this);
}
public function sorted(Query\Sort|Closure $orderBy): TabularDataReader
{
return (new Statement())->orderBy($orderBy)->process($this);
}
public function select(string|int ...$columns): TabularDataReader
{
if ([] === $columns) {
return $this;
}
$recordsHeader = $this->getHeader();
$hasHeader = [] !== $recordsHeader;
$selectColumn = function (array $header, string|int $field) use ($recordsHeader, $hasHeader): array {
if (is_string($field)) {
$index = array_search($field, $recordsHeader, true);
if (false === $index) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$index] = $field;
return $header;
}
if ($hasHeader && !array_key_exists($field, $recordsHeader)) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$field] = $recordsHeader[$field] ?? $field;
return $header;
};
/** @var array<string> $header */
$header = array_reduce($columns, $selectColumn, []);
$callback = function (array $record) use ($header): array {
$element = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$element[$headerName] = $row[$offset] ?? null;
}
return $element;
};
return new self(new MapIterator($this, $callback), $hasHeader ? $header : []);
}
public function selectAllExcept(string|int ...$columns): TabularDataReader
{
if ([] === $columns) {
return $this;
}
$recordsHeader = $this->getHeader();
$hasHeader = [] !== $recordsHeader;
$selectColumnsToSkip = function (array $res, string|int $column) use ($recordsHeader, $hasHeader): array {
if ($hasHeader) {
if (is_string($column)) {
$index = array_search($column, $recordsHeader, true);
if (false === $index) {
throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__);
}
$res[$index] = 1;
return $res;
}
if (!array_key_exists($column, $recordsHeader)) {
throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__);
}
$res[$column] = 1;
return $res;
}
if (!is_int($column)) {
throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__);
}
$res[$column] = 1;
return $res;
};
/** @var array<int> $columnsToSkip */
$columnsToSkip = array_reduce($columns, $selectColumnsToSkip, []);
$callback = function (array $record) use ($columnsToSkip): array {
$element = [];
$index = 0;
foreach ($record as $name => $value) {
if (!array_key_exists($index, $columnsToSkip)) {
$element[$name] = $value;
}
++$index;
}
return $element;
};
$newHeader = [];
if ($hasHeader) {
$newHeader = array_values(
array_filter(
$recordsHeader,
fn (string|int $key) => !array_key_exists($key, $columnsToSkip),
ARRAY_FILTER_USE_KEY
)
);
}
return new self(new MapIterator($this, $callback), $newHeader);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract all found fragment identifiers for the specifield tabular data
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @return iterable<int, TabularDataReader>
*/
public function matching(string $expression): iterable
{
return (new FragmentFinder())->findAll($expression, $this);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or returns null
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
*/
public function matchingFirst(string $expression): ?TabularDataReader
{
return (new FragmentFinder())->findFirst($expression, $this);
}
/**
* EXPERIMENTAL WARNING! This method implementation will change in the next major point release.
*
* Extract the first found fragment identifier of the tabular data or fail
*
* @experimental since version 9.12.0
*
* @throws SyntaxError
* @throws FragmentNotFound
*/
public function matchingFirstOrFail(string $expression): TabularDataReader
{
return (new FragmentFinder())->findFirstOrFail($expression, $this);
}
/**
* @param array<string> $header
*
* @throws Exception
*
* @return Iterator<array-key, TValue>
*/
public function getRecords(array $header = []): Iterator
{
return $this->combineHeader($this->prepareHeader($header));
}
/**
* @template T of object
* @param class-string<T> $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
* @return iterator<T>
*/
public function getRecordsAsObject(string $className, array $header = []): Iterator
{
$header = $this->prepareHeader($header);
return Denormalizer::assignAll(
$className,
$this->combineHeader($header),
$header
);
}
/**
* @param array<string> $header
*
* @throws SyntaxError
* @return array<string>
*/
protected function prepareHeader(array $header): array
{
$header === array_filter($header, is_string(...)) || throw SyntaxError::dueToInvalidHeaderColumnNames();
$header = $this->validateHeader($header);
if ([] === $header) {
$header = $this->header;
}
return $header;
}
/**
* Combines the header to each record if present.
*
* @param array<array-key, string|int> $header
*
* @return Iterator<array-key, TValue>
*/
protected function combineHeader(array $header): Iterator
{
return match (true) {
[] === $header => $this->records,
default => new MapIterator($this->records, function (array $record) use ($header): array {
$assocRecord = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$assocRecord[$headerName] = $row[$offset] ?? null;
}
return $assocRecord;
}),
};
}
public function count(): int
{
return iterator_count($this->records);
}
public function jsonSerialize(): array
{
return array_values([...$this->records]);
}
public function first(): array
{
return $this->nth(0);
}
public function value(int|string $column = 0): mixed
{
return match (true) {
is_string($column) => $this->first()[$column] ?? null,
default => array_values($this->first())[$column] ?? null,
};
}
public function nth(int $nth): array
{
0 <= $nth || throw InvalidArgument::dueToInvalidRecordOffset($nth, __METHOD__);
$iterator = new LimitIterator($this->getIterator(), $nth, 1);
$iterator->rewind();
/** @var array|null $result */
$result = $iterator->current();
return $result ?? [];
}
/**
* @param class-string $className
*
* @throws InvalidArgument
*/
public function nthAsObject(int $nth, string $className, array $header = []): ?object
{
$header = $this->prepareHeader($header);
$record = $this->nth($nth);
if ([] === $record) {
return null;
}
if ([] === $header || $this->header === $header) {
return Denormalizer::assign($className, $record);
}
$row = array_values($record);
$record = [];
foreach ($header as $offset => $headerName) {
$record[$headerName] = $row[$offset] ?? null;
}
return Denormalizer::assign($className, $record);
}
/**
* @param class-string $className
*
* @throws InvalidArgument
*/
public function firstAsObject(string $className, array $header = []): ?object
{
return $this->nthAsObject(0, $className, $header);
}
public function fetchColumn(string|int $index = 0): Iterator
{
return $this->yieldColumn(
$this->getColumnIndex($index, 'offset', __METHOD__)
);
}
protected function yieldColumn(string|int $offset): Generator
{
yield from new MapIterator(
new CallbackFilterIterator($this->records, fn (array $record): bool => isset($record[$offset])),
fn (array $record): string => $record[$offset]
);
}
/**
* Filters a column name against the header if any.
*
* @throws InvalidArgument if the field is invalid or not found
*/
protected function getColumnIndex(string|int $field, string $type, string $method): string|int
{
return match (true) {
is_string($field) => $this->getColumnIndexByValue($field, $type, $method),
default => $this->getColumnIndexByKey($field, $type, $method),
};
}
/**
* Returns the selected column name.
*
* @throws InvalidArgument if the column is not found
*/
protected function getColumnIndexByValue(string $value, string $type, string $method): string
{
return match (true) {
false === array_search($value, $this->header, true) => throw InvalidArgument::dueToInvalidColumnIndex($value, $type, $method),
default => $value,
};
}
/**
* Returns the selected column name according to its offset.
*
* @throws InvalidArgument if the field is invalid or not found
*/
protected function getColumnIndexByKey(int $index, string $type, string $method): int|string
{
return match (true) {
$index < 0 => throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method),
[] === $this->header => $index,
false !== ($value = array_search($index, array_flip($this->header), true)) => $value,
default => throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method),
};
}
public function fetchPairs(string|int $offset_index = 0, string|int $value_index = 1): Iterator
{
$offset = $this->getColumnIndex($offset_index, 'offset', __METHOD__);
$value = $this->getColumnIndex($value_index, 'value', __METHOD__);
$iterator = new MapIterator(
new CallbackFilterIterator($this->records, fn (array $record): bool => isset($record[$offset])),
fn (array $record): array => [$record[$offset], $record[$value] ?? null]
);
/** @var array{0:int|string, 1:string|null} $pair */
foreach ($iterator as $pair) {
yield $pair[0] => $pair[1];
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws Exception
*
* @deprecated since version 9.23.0
* @codeCoverageIgnore
*
* @see ResultSet::fetchColumn()
*/
#[Deprecated(message:'use League\Csv\Resultset::fetchColumn() instead', since:'league/csv:9.23.0')]
public function fetchColumnByName(string $name): Iterator
{
return $this->yieldColumn(
$this->getColumnIndexByValue($name, 'name', __METHOD__)
);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws Exception
*
* @deprecated since version 9.23.0
* @codeCoverageIgnore
*
* @see ResultSet::fetchColumn()
*/
#[Deprecated(message:'use League\Csv\Resultset::fetchColumn() instead', since:'league/csv:9.23.0')]
public function fetchColumnByOffset(int $offset): Iterator
{
return $this->yieldColumn(
$this->getColumnIndexByKey($offset, 'offset', __METHOD__)
);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see ResultSet::nth()
* @deprecated since version 9.9.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\Resultset::nth() instead', since:'league/csv:9.9.0')]
public function fetchOne(int $nth_record = 0): array
{
return $this->nth($nth_record);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see ResultSet::getRecordsAsObject()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*
* @param class-string $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*/
#[Deprecated(message:'use League\Csv\ResultSet::getRecordsAsObject() instead', since:'league/csv:9.15.0')]
public function getObjects(string $className, array $header = []): Iterator
{
return $this->getRecordsAsObject($className, $header);
}
/**
* Returns a new instance from an object implementing the TabularDataReader interface.
*
* @throws SyntaxError
*/
#[Deprecated(message:'use League\Csv\ResultSet::from() instead', since:'league/csv:9.22.0')]
public static function createFromTabularDataReader(TabularDataReader $reader): self
{
return self::from($reader);
}
/**
* Returns a new instance from a collection without header.
*/
#[Deprecated(message:'use League\Csv\ResultSet::from() instead', since:'league/csv:9.22.0')]
public static function createFromRecords(iterable $records = []): self
{
return new self(MapIterator::toIterator($records));
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Attribute;
use Deprecated;
use ReflectionAttribute;
use ReflectionClass;
/**
* @deprecated since version 9.17.0
*
* @see MapRecord
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class AfterMapping
{
public readonly MapRecord $mapRecord;
public readonly array $methods;
#[Deprecated(message: 'use League\Csv\Serializer\MapRecord instead', since: 'league/csv:9.17.0')]
public function __construct(string ...$methods)
{
$this->mapRecord = new MapRecord($methods);
$this->methods = $this->mapRecord->afterMapping;
}
public static function from(ReflectionClass $class): ?self
{
$attributes = $class->getAttributes(self::class, ReflectionAttribute::IS_INSTANCEOF);
$nbAttributes = count($attributes);
return match (true) {
0 === $nbAttributes => null,
1 < $nbAttributes => throw new MappingFailed('Using more than one `'.self::class.'` attribute on a class property or method is not supported.'),
default => $attributes[0]->newInstance(),
};
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Serializer;
use function in_array;
enum ArrayShape: string
{
case List = 'list';
case Csv = 'csv';
case Json = 'json';
public function equals(mixed $value): bool
{
return $value instanceof self
&& $value === $this;
}
public function isOneOf(self ...$types): bool
{
return in_array($this, $types, true);
}
}

View File

@ -0,0 +1,392 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Closure;
use Deprecated;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use Throwable;
use function array_key_exists;
use function class_exists;
/**
* @internal Container for registering Closure as type and/or type alias casting
* @template TValue
*/
final class CallbackCasting implements TypeCasting
{
/** @var array<string, Closure(mixed, bool, mixed...): mixed> */
private static array $types = [];
/** @var array<string, array<string, Closure(mixed, bool, mixed...): mixed>> */
private static array $aliases = [];
private string $type;
private readonly bool $isNullable;
/** @var Closure(mixed, bool, mixed...): mixed */
private Closure $callback;
private array $options = [];
private string $message;
private readonly TypeCastingInfo $info;
public function __construct(
ReflectionProperty|ReflectionParameter $reflectionProperty,
private readonly ?string $alias = null
) {
[$this->type, $this->isNullable] = self::resolve($reflectionProperty);
$this->message = match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
};
$this->callback = fn (mixed $value, bool $isNullable, mixed ...$arguments): mixed => $value;
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->info;
}
/**
* @throws MappingFailed
*/
public function setOptions(?string $type = null, mixed ...$options): void
{
if (null === $this->alias) {
if (Type::Mixed->value === $this->type && null !== $type) {
$this->type = $type;
}
try {
$this->callback = self::resolveTypeCallback($this->type); /* @phpstan-ignore-line */
$this->options = $options;
return;
} catch (Throwable) {
}
throw new MappingFailed($this->message);
}
if (Type::Mixed->value === $this->type) {
$this->type = self::aliases()[$this->alias];
}
$this->callback = self::resolveAliasCallback($this->type, $this->alias);
$this->options = $options;
}
/**
* @return TValue
*/
public function toVariable(mixed $value): mixed
{
try {
return ($this->callback)($value, $this->isNullable, ...$this->options);
} catch (Throwable $exception) {
! $exception instanceof TypeCastingFailed || throw $exception;
null !== $value || throw TypeCastingFailed::dueToNotNullableType($this->type, $exception, $this->info);
throw TypeCastingFailed::dueToInvalidValue(match (true) {
'' === $value => 'empty string',
default => $value,
}, $this->type, $exception, $this->info);
}
}
/**
* @param Closure(mixed, bool, mixed...): TValue $callback
*/
public static function register(string $type, Closure $callback, ?string $alias = null): void
{
if (null === $alias) {
self::$types[$type] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};
return;
}
1 === preg_match('/^@\w+$/', $alias) || throw new MappingFailed("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
foreach (self::$aliases as $aliases) {
foreach ($aliases as $registeredAlias => $__) {
$alias !== $registeredAlias || throw new MappingFailed("The alias `$alias` is already registered. Please choose another name.");
}
}
self::$aliases[$type][$alias] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};
}
public static function unregisterType(string $type): bool
{
if (!array_key_exists($type, self::$types)) {
return false;
}
unset(self::$types[$type]);
return true;
}
public static function unregisterTypes(): void
{
self::$types = [];
}
public static function unregisterAlias(string $alias): bool
{
if (1 !== preg_match('/^@\w+$/', $alias)) {
return false;
}
foreach (self::$aliases as $type => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
if ($registeredAlias === $alias) {
unset(self::$aliases[$type][$registeredAlias]);
return true;
}
}
}
return false;
}
public static function unregisterAliases(): void
{
self::$aliases = [];
}
public static function unregisterAll(): void
{
self::unregisterTypes();
self::unregisterAliases();
}
public static function supportsAlias(?string $alias): bool
{
return null !== $alias && array_key_exists($alias, self::aliases());
}
public static function supportsType(?string $type): bool
{
if (null === $type) {
return false;
}
try {
self::resolveTypeCallback($type); /* @phpstan-ignore-line */
return true;
} catch (Throwable) {
return false;
}
}
/**
* @return array<string>
*/
public static function types(): array
{
return array_keys(self::$types);
}
/**
* @return array<string, string>
*/
public static function aliases(): array
{
$res = [];
foreach (self::$aliases as $registeredType => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
$res[$registeredAlias] = $registeredType;
}
}
return $res;
}
public static function supports(ReflectionParameter|ReflectionProperty $reflectionProperty, ?string $alias = null): bool
{
$propertyTypeList = self::getTypes($reflectionProperty->getType());
if ([] === $propertyTypeList && self::supportsAlias($alias)) {
return true;
}
foreach ($propertyTypeList as $propertyType) {
$type = $propertyType->getName();
if (null === $alias) {
if (self::supportsType($type)) {
return true;
}
continue;
}
if (self::aliasSupportsType($type) || (Type::Mixed->value === $type && self::supportsAlias($alias))) {
return true;
}
}
return false;
}
private static function aliasSupportsType(string $type): bool
{
foreach (self::aliases() as $registeredType) {
if ($type === $registeredType) {
return true;
}
try {
if ((new ReflectionClass($type))->implementsInterface($registeredType)) { /* @phpstan-ignore-line */
return true;
}
} catch (Throwable) {
}
}
return false;
}
/**
* @param class-string $type
*/
private static function resolveTypeCallback(string $type): Closure
{
foreach (self::$types as $registeredType => $callback) {
if ($type === $registeredType) {
return $callback;
}
try {
$reflType = new ReflectionClass($type);
if ($reflType->implementsInterface($registeredType)) {
return $callback;
}
} catch (Throwable) {
}
}
throw new MappingFailed('The `'.$type.'` could not be resolved.');
}
private static function resolveAliasCallback(string $type, string $alias): Closure
{
$rType = self::aliases()[$alias] ?? null;
if (isset($rType)) {
return self::$aliases[$rType][$alias];
}
foreach (self::aliases() as $aliasName => $registeredType) {
try {
$reflType = new ReflectionClass($type); /* @phpstan-ignore-line */
if ($reflType->implementsInterface($registeredType)) {
return self::$aliases[$registeredType][$aliasName];
}
} catch (Throwable) {
}
}
throw new MappingFailed('The `'.$type.'` could not be resolved.');
}
/**
* @throws MappingFailed
*
* @return array{0:string, 1:bool}
*/
private static function resolve(ReflectionParameter|ReflectionProperty $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed->value, true];
}
$types = self::getTypes($reflectionProperty->getType());
$type = null;
$isNullable = false;
$hasMixed = false;
foreach ($types as $foundType) {
if (!$isNullable && $foundType->allowsNull()) {
$isNullable = true;
}
if (null === $type) {
$instanceName = $foundType->getName();
if (self::supportsType($instanceName) || array_key_exists($instanceName, self::$aliases)) {
$type = $foundType;
}
if (true !== $hasMixed && Type::Mixed->value === $instanceName) {
$hasMixed = true;
}
}
}
return match (true) {
$type instanceof ReflectionNamedType => [$type->getName(), $isNullable],
$hasMixed => [Type::Mixed->value, true],
default => throw new MappingFailed(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
}),
};
}
/**
* @return array<ReflectionNamedType>
*/
private static function getTypes(?ReflectionType $type): array
{
return match (true) {
$type instanceof ReflectionNamedType => [$type],
$type instanceof ReflectionUnionType => array_filter(
$type->getTypes(),
fn (ReflectionType $innerType) => $innerType instanceof ReflectionNamedType
),
default => [],
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.13.0
* @see CallbackCasting::unregisterType()
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\Serializer\CallbackCasting::unregisterType() instead', since:'league/csv:9.13.0')]
public static function unregister(string $type): bool
{
return self::unregisterType($type);
}
}

View File

@ -0,0 +1,228 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use JsonException;
use League\Csv\Exception;
use League\Csv\Reader;
use ReflectionParameter;
use ReflectionProperty;
use function array_map;
use function explode;
use function filter_var;
use function is_array;
use function json_decode;
use function strlen;
use const FILTER_REQUIRE_ARRAY;
use const JSON_THROW_ON_ERROR;
/**
* @implements TypeCasting<array|null>
*/
final class CastToArray implements TypeCasting
{
private readonly Type $type;
private readonly bool $isNullable;
private ArrayShape $shape;
private int $filterFlag;
/** @var non-empty-string */
private string $separator = ',';
private string $delimiter = '';
private string $enclosure = '"';
/** @var int<1, max> $depth */
private int $depth = 512;
private int $flags = 0;
private ?array $default = null;
private bool $trimElementValueBeforeCasting = false;
private ?int $headerOffset = null;
private readonly TypeCastingInfo $info;
/**
* @throws MappingFailed
*/
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
$this->shape = ArrayShape::List;
$this->filterFlag = Type::String->filterFlag();
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->info;
}
/**
* @param non-empty-string $delimiter
* @param non-empty-string $separator
* @param int<1, max> $depth
*
* @throws MappingFailed
*/
public function setOptions(
?array $default = null,
ArrayShape|string $shape = ArrayShape::List,
string $separator = ',',
string $delimiter = ',',
string $enclosure = '"',
int $depth = 512,
int $flags = 0,
Type|string $type = Type::String,
bool $trimElementValueBeforeCasting = false,
?int $headerOffset = null,
): void {
if (!$shape instanceof ArrayShape) {
$shape = ArrayShape::tryFrom($shape) ?? throw new MappingFailed('Unable to resolve the array shape; Verify your options arguments.');
}
if (!$type instanceof Type) {
$type = Type::tryFrom($type) ?? throw new MappingFailed('Unable to resolve the array value type; Verify your options arguments.');
}
$this->shape = $shape;
$this->depth = $depth;
$this->separator = $separator;
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->flags = $flags;
$this->default = $default;
$this->filterFlag = match (true) {
1 > $this->depth && $this->shape->equals(ArrayShape::Json) => throw new MappingFailed('the json depth can not be less than 1.'),
1 > strlen($this->separator) && $this->shape->equals(ArrayShape::List) => throw new MappingFailed('expects separator to be a non-empty string for list conversion; empty string given.'),
1 !== strlen($this->delimiter) && $this->shape->equals(ArrayShape::Csv) => throw new MappingFailed('expects delimiter to be a single character for CSV conversion; `'.$this->delimiter.'` given.'),
1 !== strlen($this->enclosure) && $this->shape->equals(ArrayShape::Csv) => throw new MappingFailed('expects enclosure to be a single character; `'.$this->enclosure.'` given.'),
default => $this->resolveFilterFlag($type),
};
$this->trimElementValueBeforeCasting = $trimElementValueBeforeCasting;
$this->headerOffset = $headerOffset;
}
public function toVariable(mixed $value): ?array
{
if (null === $value) {
return match (true) {
$this->isNullable,
Type::Mixed->equals($this->type) => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->type->value, info: $this->info),
};
}
if ('' === $value) {
return [];
}
if (is_array($value)) {
return $value;
}
if (!is_string($value)) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value, info: $this->info);
}
if ($this->shape->equals(ArrayShape::Json)) {
try {
$data = json_decode($value, true, $this->depth, $this->flags | JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value, $exception, $this->info);
}
if (!is_array($data)) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value, info: $this->info);
}
return $data;
}
if ($this->shape->equals(ArrayShape::Csv)) {
try {
$data = Reader::createFromString($value);
$data->setDelimiter($this->delimiter);
$data->setEnclosure($this->enclosure);
$data->setEscape('');
$data->setHeaderOffset($this->headerOffset);
if ($this->trimElementValueBeforeCasting) {
$data->addFormatter($this->trimString(...));
}
$data->addFormatter($this->filterElement(...));
return [...$data];
} catch (Exception $exception) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value, $exception, $this->info);
}
}
$data = explode($this->separator, $value);
return $this->filterElement(match (true) {
$this->trimElementValueBeforeCasting => $this->trimString($data),
default => $data,
});
}
private function trimString(array $record): array
{
return array_map(
fn (mixed $value): mixed => is_string($value) ? trim($value) : $value,
$record
);
}
private function filterElement(array $record): array
{
return filter_var($record, $this->filterFlag, FILTER_REQUIRE_ARRAY);
}
/**
* @throws MappingFailed if the type is not supported
*/
private function resolveFilterFlag(?Type $type): int
{
return match (true) {
$this->shape->equals(ArrayShape::Json) => Type::String->filterFlag(),
$type instanceof Type && $type->isOneOf(Type::Bool, Type::True, Type::False, Type::String, Type::Float, Type::Int) => $type->filterFlag(),
default => throw new MappingFailed('Only scalar type are supported for `array` value casting.'),
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Array, Type::Iterable)) {
$type = $found;
}
}
if (null === $type) {
throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'array', 'iterable', 'mixed');
}
return [$type[0], $isNullable];
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?bool>
*/
final class CastToBool implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private readonly TypeCastingInfo $info;
private ?bool $default = null;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function setOptions(
?bool $default = null,
bool $emptyStringAsNull = false,
): void {
$this->default = $default;
}
public function info(): TypeCastingInfo
{
return $this->info;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): ?bool
{
$returnValue = match (true) {
is_bool($value) => $value,
null !== $value => filter_var($value, Type::Bool->filterFlag()),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType('boolean', info: $this->info),
};
return match (true) {
Type::True->equals($this->type) && true !== $returnValue && !$this->isNullable,
Type::False->equals($this->type) && false !== $returnValue && !$this->isNullable => throw TypeCastingFailed::dueToInvalidValue(match (true) {
null === $value => 'null',
'' === $value => 'empty string',
default => $value,
}, $this->type->value, info: $this->info),
default => $returnValue,
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Bool, Type::True, Type::False)) {
$type = $found;
}
}
if (null === $type) {
throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'bool', 'mixed');
}
return [$type[0], $isNullable];
}
}

View File

@ -0,0 +1,159 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use ReflectionClass;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use function class_exists;
use function is_string;
/**
* @implements TypeCasting<DateTimeImmutable|DateTime|null>
*/
final class CastToDate implements TypeCasting
{
/** @var class-string */
private string $class;
private readonly bool $isNullable;
private DateTimeImmutable|DateTime|null $default = null;
private readonly Type $type;
private readonly TypeCastingInfo $info;
private ?DateTimeZone $timezone = null;
private ?string $format = null;
/**
* @throws MappingFailed
*/
public function __construct(
ReflectionProperty|ReflectionParameter $reflectionProperty,
) {
[$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty);
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
/**
* @param ?class-string $className
*
* @throws MappingFailed
*/
public function setOptions(
?string $default = null,
?string $format = null,
DateTimeZone|string|null $timezone = null,
?string $className = null,
): void {
$this->class = match (true) {
!interface_exists($this->class) && !Type::Mixed->equals($this->type) => $this->class,
DateTimeInterface::class === $this->class && null === $className => DateTimeImmutable::class,
interface_exists($this->class) && null !== $className && class_exists($className) && (new ReflectionClass($className))->implementsInterface($this->class) => $className,
default => throw new MappingFailed('`'.$this->info->targetName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'),
};
try {
$this->format = $format;
$this->timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone;
$this->default = (null !== $default) ? $this->cast($default) : $default;
} catch (Throwable $exception) {
throw new MappingFailed('The `timezone` and/or `format` options used for `'.self::class.'` are invalud.', 0, $exception);
}
}
public function info(): TypeCastingInfo
{
return $this->info;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): DateTimeImmutable|DateTime|null
{
return match (true) {
null !== $value && '' !== $value => $this->cast($value),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->class, info: $this->info),
};
}
/**
* @throws TypeCastingFailed
*/
private function cast(mixed $value): DateTimeImmutable|DateTime
{
if ($value instanceof DateTimeInterface) {
if ($value instanceof $this->class) {
return $value;
}
return ($this->class)::createFromInterface($value);
}
is_string($value) || throw TypeCastingFailed::dueToInvalidValue($value, $this->class, info: $this->info);
try {
$date = null !== $this->format ?
($this->class)::createFromFormat($this->format, $value, $this->timezone) :
new ($this->class)($value, $this->timezone);
if (false === $date) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->class);
}
} catch (Throwable $exception) {
if ($exception instanceof TypeCastingFailed) {
throw $exception;
}
throw TypeCastingFailed::dueToInvalidValue($value, $this->class, $exception, $this->info);
}
return $date;
}
/**
* @throws MappingFailed
*
* @return array{0:Type, 1:class-string<DateTimeInterface>, 2:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, DateTimeInterface::class, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Date)) {
$type = $found;
}
}
null !== $type || throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, DateTimeInterface::class, 'mixed');
/** @var class-string<DateTimeInterface> $className */
$className = $type[1]->getName();
return [$type[0], $className, $isNullable];
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use BackedEnum;
use ReflectionEnum;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use UnitEnum;
/**
* @implements TypeCasting<BackedEnum|UnitEnum|null>
*/
class CastToEnum implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private ?UnitEnum $default = null;
private readonly TypeCastingInfo $info;
/** @var class-string<UnitEnum|BackedEnum> */
private string $class;
/**
* @throws MappingFailed
*/
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty);
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->info;
}
/**
* @param ?class-string<UnitEnum|BackedEnum> $className *
*
* @throws MappingFailed
*/
public function setOptions(
?string $default = null,
?string $className = null,
bool $emptyStringAsNull = false,
): void {
if (Type::Mixed->equals($this->type) || in_array($this->class, [BackedEnum::class , UnitEnum::class], true)) {
(null !== $className && enum_exists($className)) || throw new MappingFailed('`'.$this->info->targetName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.');
$this->class = $className;
}
try {
$this->default = (null !== $default) ? $this->cast($default) : $default;
} catch (TypeCastingFailed $exception) {
throw new MappingFailed(message:'The `default` option is invalid.', previous: $exception);
}
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): BackedEnum|UnitEnum|null
{
return match (true) {
null !== $value => $this->cast($value),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->class, info: $this->info),
};
}
/**
* @throws TypeCastingFailed
*/
private function cast(mixed $value): BackedEnum|UnitEnum
{
if ($value instanceof $this->class) {
return $value;
}
is_string($value) || throw throw TypeCastingFailed::dueToInvalidValue($value, $this->class, info: $this->info);
try {
$enum = new ReflectionEnum($this->class);
if (!$enum->isBacked()) {
return $enum->getCase($value)->getValue();
}
$backedValue = 'int' === $enum->getBackingType()->getName() ? filter_var($value, Type::Int->filterFlag()) : $value;
return $this->class::from($backedValue); /* @phpstan-ignore-line */
} catch (Throwable $exception) {
throw throw TypeCastingFailed::dueToInvalidValue($value, $this->class, $exception, $this->info);
}
}
/**
* @return array{0:Type, 1:class-string<UnitEnum|BackedEnum>, 2:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, UnitEnum::class, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Enum)) {
$type = $found;
}
}
null !== $type || throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'enum', 'mixed');
/** @var class-string<UnitEnum|BackedEnum> $className */
$className = $type[1]->getName();
return [$type[0], $className, $isNullable];
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?float>
*/
final class CastToFloat implements TypeCasting
{
private readonly bool $isNullable;
private ?float $default = null;
private readonly TypeCastingInfo $info;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
$this->isNullable = $this->init($reflectionProperty);
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->info;
}
public function setOptions(
int|float|null $default = null,
bool $emptyStringAsNull = false,
): void {
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): ?float
{
if (null === $value) {
return match ($this->isNullable) {
true => $this->default,
false => throw TypeCastingFailed::dueToNotNullableType('float', info: $this->info),
};
}
is_scalar($value) || throw TypeCastingFailed::dueToInvalidValue($value, Type::Int->value, info: $this->info);
$float = filter_var($value, Type::Float->filterFlag());
return match ($float) {
false => throw TypeCastingFailed::dueToInvalidValue($value, Type::Float->value, info: $this->info),
default => $float,
};
}
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): bool
{
if (null === $reflectionProperty->getType()) {
return true;
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Float)) {
$type = $found;
}
}
null !== $type || throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'float', 'null', 'mixed');
return $isNullable;
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?int>
*/
final class CastToInt implements TypeCasting
{
private readonly bool $isNullable;
private ?int $default = null;
private readonly TypeCastingInfo $info;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
$this->isNullable = $this->init($reflectionProperty);
$this->info = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->info;
}
public function setOptions(
?int $default = null,
bool $emptyStringAsNull = false,
): void {
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): ?int
{
if (null === $value) {
return match ($this->isNullable) {
true => $this->default,
false => throw TypeCastingFailed::dueToNotNullableType('integer', info: $this->info),
};
}
is_scalar($value) || throw TypeCastingFailed::dueToInvalidValue($value, Type::Int->value, info: $this->info);
$int = filter_var($value, Type::Int->filterFlag());
return match ($int) {
false => throw TypeCastingFailed::dueToInvalidValue($value, Type::Int->value, info: $this->info),
default => $int,
};
}
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): bool
{
if (null === $reflectionProperty->getType()) {
return true;
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Int, Type::Float)) {
$type = $found;
}
}
null !== $type || throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'int', 'float', 'null', 'mixed');
return $isNullable;
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionParameter;
use ReflectionProperty;
/**
* @implements TypeCasting<?string>
*/
final class CastToString implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private ?string $default = null;
private readonly TypeCastingInfo $variableName;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
$this->variableName = TypeCastingInfo::fromAccessor($reflectionProperty);
}
public function info(): TypeCastingInfo
{
return $this->variableName;
}
public function setOptions(
?string $default = null,
bool $emptyStringAsNull = false,
): void {
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(mixed $value): ?string
{
$returnedValue = match (true) {
is_string($value) => $value,
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->type->value, info: $this->variableName),
};
return match (true) {
Type::Null->equals($this->type) && null !== $returnedValue => throw TypeCastingFailed::dueToInvalidValue(match (true) {
null === $value => 'null',
'' === $value => 'empty string',
default => $value,
}, $this->type->value, info: $this->variableName),
default => $returnedValue,
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::String, Type::Mixed, Type::Null)) {
$type = $found;
}
}
null !== $type || throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'string', 'mixed', 'null');
return [$type[0], $isNullable];
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionProperty;
use RuntimeException;
final class DenormalizationFailed extends RuntimeException implements SerializationFailed
{
public static function dueToUninitializedProperty(ReflectionProperty $reflectionProperty): self
{
return new self('The property '.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().' is not initialized; its value is missing from the source data.');
}
}

View File

@ -0,0 +1,461 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Closure;
use Deprecated;
use Iterator;
use League\Csv\MapIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use function array_search;
use function array_values;
use function count;
use function is_int;
final class Denormalizer
{
private static bool $convertEmptyStringToNull = true;
private readonly ReflectionClass $class;
/** @var array<ReflectionProperty> */
private readonly array $properties;
/** @var array<PropertySetter> */
private readonly array $propertySetters;
/** @var array<ReflectionMethod> */
private readonly array $afterMappingCalls;
private readonly ?MapRecord $mapRecord;
/**
* @param class-string $className
* @param array<string> $propertyNames
*
* @throws MappingFailed
*/
public function __construct(string $className, array $propertyNames = [])
{
$this->class = $this->setClass($className);
$this->properties = $this->class->getProperties();
$this->mapRecord = MapRecord::tryFrom($this->class);
$this->propertySetters = $this->setPropertySetters($propertyNames);
$this->afterMappingCalls = $this->setAfterMappingCalls();
}
/**
* @deprecated since version 9.17.0
*
* @see MapRecord::$convertEmptyStringToNull
* @see MapCell::$convertEmptyStringToNull
*
* Enables converting empty string to the null value.
*/
#[Deprecated(message:'use League\Csv\Serializer\MapRecord::$convertEmptyStringToNull or League\Csv\Serializer\MapCell::$convertEmptyStringToNullinstead', since:'league/csv:9.17.0')]
public static function allowEmptyStringAsNull(): void
{
self::$convertEmptyStringToNull = true;
}
/**
* @deprecated since version 9.17.0
*
* @see MapRecord::$convertEmptyStringToNull
* @see MapCell::$convertEmptyStringToNull
*
* Disables converting empty string to the null value.
*/
#[Deprecated(message:'use League\Csv\Serializer\MapRecord::$convertEmptyStringToNull or League\Csv\Serializer\MapCell::$convertEmptyStringToNullinstead', since:'league/csv:9.17.0')]
public static function disallowEmptyStringAsNull(): void
{
self::$convertEmptyStringToNull = false;
}
/**
* Register a global type conversion callback to convert a field into a specific type.
*
* @throws MappingFailed
*/
public static function registerType(string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback);
}
/**
* Unregister a global type conversion callback to convert a field into a specific type.
*
*
*/
public static function unregisterType(string $type): bool
{
return CallbackCasting::unregisterType($type);
}
public static function unregisterAllTypes(): void
{
CallbackCasting::unregisterTypes();
}
/**
* Register a callback to convert a field into a specific type.
*
* @throws MappingFailed
*/
public static function registerAlias(string $alias, string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback, $alias);
}
public static function unregisterAlias(string $alias): bool
{
return CallbackCasting::unregisterAlias($alias);
}
public static function unregisterAllAliases(): void
{
CallbackCasting::unregisterAliases();
}
public static function unregisterAll(): void
{
CallbackCasting::unregisterAll();
}
/**
* @return array<string>
*/
public static function types(): array
{
$default = [...array_column(Type::cases(), 'value'), ...CallbackCasting::types()];
return array_values(array_unique($default));
}
/**
* @return array<string, string>
*/
public static function aliases(): array
{
return CallbackCasting::aliases();
}
public static function supportsAlias(string $alias): bool
{
return CallbackCasting::supportsAlias($alias);
}
/**
* @param class-string $className
* @param array<array-key, mixed> $record
*
* @throws DenormalizationFailed
* @throws MappingFailed
* @throws ReflectionException
* @throws TypeCastingFailed
*/
public static function assign(string $className, array $record): object
{
return (new self($className, array_keys($record)))->denormalize($record);
}
/**
* @param class-string $className
* @param array<string> $propertyNames
*
* @throws MappingFailed
* @throws TypeCastingFailed
*/
public static function assignAll(string $className, iterable $records, array $propertyNames = []): Iterator
{
return (new self($className, $propertyNames))->denormalizeAll($records);
}
public function denormalizeAll(iterable $records): Iterator
{
return MapIterator::fromIterable($records, $this->denormalize(...));
}
/**
* @throws DenormalizationFailed
* @throws ReflectionException
* @throws TypeCastingFailed
*/
public function denormalize(array $record): object
{
$object = $this->class->newInstanceWithoutConstructor();
$values = array_values($record);
foreach ($this->propertySetters as $propertySetter) {
$propertySetter($object, $values);
}
foreach ($this->afterMappingCalls as $callback) {
$callback->invoke($object);
}
foreach ($this->properties as $property) {
$property->isInitialized($object) || throw DenormalizationFailed::dueToUninitializedProperty($property);
}
return $object;
}
/**
* @param class-string $className
*
* @throws MappingFailed
*/
private function setClass(string $className): ReflectionClass
{
class_exists($className) || throw new MappingFailed('The class `'.$className.'` can not be denormalized; The class does not exist or could not be found.');
$class = new ReflectionClass($className);
if ($class->isInternal() && $class->isFinal()) {
throw new MappingFailed('The class `'.$className.'` can not be denormalized; PHP internal class marked as final can not be instantiated without using the constructor.');
}
return $class;
}
/**
* @param array<string> $propertyNames
*
* @throws MappingFailed
*
* @return array<PropertySetter>
*/
private function setPropertySetters(array $propertyNames): array
{
$propertySetters = [];
$methodNames = array_map(fn (string $propertyName) => 'set'.ucfirst($propertyName), $propertyNames);
foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) {
$attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF);
$propertySetter = match (count($attributes)) {
0 => $this->autoDiscoverPropertySetter($accessor, $propertyNames, $methodNames),
1 => $this->findPropertySetter($attributes[0]->newInstance(), $accessor, $propertyNames),
default => throw new MappingFailed('Using more than one `'.MapCell::class.'` attribute on a class property or method is not supported.'),
};
if (null !== $propertySetter) {
$propertySetters[] = $propertySetter;
}
}
return match ([]) {
$propertySetters => throw new MappingFailed('No property or method from `'.$this->class->getName().'` could be used for denormalization.'),
default => $propertySetters,
};
}
/**
* @return array<ReflectionMethod>
*/
private function setAfterMappingCalls(): array
{
return $this->mapRecord?->afterMappingMethods($this->class)
?? AfterMapping::from($this->class)?->mapRecord->afterMappingMethods($this->class) /* @phpstan-ignore-line */
?? [];
}
/**
* @param array<string> $propertyNames
* @param array<?string> $methodNames
*
* @throws MappingFailed
*/
private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty $accessor, array $propertyNames, array $methodNames): ?PropertySetter
{
if ($accessor->isStatic() || !$accessor->isPublic()) {
return null;
}
if ($accessor instanceof ReflectionMethod) {
if ($accessor->isConstructor()) {
return null;
}
if ([] === $accessor->getParameters()) {
return null;
}
if (1 < $accessor->getNumberOfRequiredParameters()) {
return null;
}
}
/** @var int|false $offset */
/** @var ReflectionParameter|ReflectionProperty $reflectionProperty */
[$offset, $reflectionProperty] = match (true) {
$accessor instanceof ReflectionMethod => [array_search($accessor->getName(), $methodNames, true), $accessor->getParameters()[0]],
$accessor instanceof ReflectionProperty => [array_search($accessor->getName(), $propertyNames, true), $accessor],
};
return match (true) {
false === $offset,
null === $reflectionProperty->getType() => null,
default => new PropertySetter(
$accessor,
$offset,
$this->resolveTypeCasting($reflectionProperty),
$this->mapRecord?->convertEmptyStringToNull ?? self::$convertEmptyStringToNull,
$this->mapRecord?->trimFieldValueBeforeCasting ?? false
),
};
}
/**
* @param array<string> $propertyNames
*
* @throws MappingFailed
*/
private function findPropertySetter(MapCell $mapCell, ReflectionMethod|ReflectionProperty $accessor, array $propertyNames): ?PropertySetter
{
if ($mapCell->ignore) {
return null;
}
$typeCaster = $this->resolveTypeCaster($mapCell, $accessor);
$offset = $mapCell->column ?? match (true) {
$accessor instanceof ReflectionMethod => $this->getMethodFirstArgument($accessor)->getName(),
$accessor instanceof ReflectionProperty => $accessor->getName(),
};
if (!is_int($offset)) {
if ([] === $propertyNames) {
throw new MappingFailed('offset as string are only supported if the property names list is not empty.');
}
/** @var int<0, max>|false $index */
$index = array_search($offset, $propertyNames, true);
if (false === $index) {
throw new MappingFailed('The `'.$offset.'` property could not be found in the property names list; Please verify your property names list.');
}
$offset = $index;
}
$reflectionProperty = match (true) {
$accessor instanceof ReflectionMethod => $accessor->getParameters()[0],
$accessor instanceof ReflectionProperty => $accessor,
};
$convertEmptyStringToNull = $mapCell->convertEmptyStringToNull
?? $this->mapRecord?->convertEmptyStringToNull
?? self::$convertEmptyStringToNull;
$trimFieldValueBeforeCasting = $mapCell->trimFieldValueBeforeCasting
?? $this->mapRecord?->trimFieldValueBeforeCasting
?? false;
return match (true) {
0 > $offset => throw new MappingFailed('offset integer position can only be positive or equals to 0; received `'.$offset.'`'),
[] !== $propertyNames && $offset > count($propertyNames) - 1 => throw new MappingFailed('offset integer position can not exceed property names count.'),
null === $typeCaster => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($reflectionProperty, $mapCell->options), $convertEmptyStringToNull, $trimFieldValueBeforeCasting),
default => new PropertySetter($accessor, $offset, $this->getTypeCasting($typeCaster, $reflectionProperty, $mapCell->options), $convertEmptyStringToNull, $trimFieldValueBeforeCasting),
};
}
/**
* @throws MappingFailed
*/
private function getMethodFirstArgument(ReflectionMethod $reflectionMethod): ReflectionParameter
{
$arguments = $reflectionMethod->getParameters();
return match (true) {
[] === $arguments => throw new MappingFailed('The method `'.$reflectionMethod->getDeclaringClass()->getName().'::'.$reflectionMethod->getName().'` does not use parameters.'),
1 < $reflectionMethod->getNumberOfRequiredParameters() => throw new MappingFailed('The method `'.$reflectionMethod->getDeclaringClass()->getName().'::'.$reflectionMethod->getName().'` has too many required parameters.'),
default => $arguments[0]
};
}
/**
* @throws MappingFailed
*/
private function getTypeCasting(
string $typeCaster,
ReflectionProperty|ReflectionParameter $reflectionProperty,
array $options
): TypeCasting {
try {
/** @var TypeCasting $cast */
$cast = match (str_starts_with($typeCaster, CallbackCasting::class.'@')) {
true => new CallbackCasting($reflectionProperty, substr($typeCaster, strlen(CallbackCasting::class))),
false => new $typeCaster($reflectionProperty),
};
$cast->setOptions(...$options);
return $cast;
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
/**
* @throws MappingFailed
*/
private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $reflectionProperty, array $options = []): TypeCasting
{
$castResolver = function (ReflectionProperty|ReflectionParameter $reflectionProperty, $options): CallbackCasting {
$cast = new CallbackCasting($reflectionProperty);
$cast->setOptions(...$options);
return $cast;
};
try {
return match (true) {
CallbackCasting::supports($reflectionProperty) => $castResolver($reflectionProperty, $options),
default => Type::resolve($reflectionProperty, $options),
};
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
public function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionProperty $accessor): ?string
{
/** @var ?class-string<TypeCasting> $typeCaster */
$typeCaster = $mapCell->cast;
if (null === $typeCaster) {
return null;
}
if (class_exists($typeCaster)) {
if (!(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}
return $typeCaster;
}
if ($accessor instanceof ReflectionMethod) {
$accessor = $accessor->getParameters()[0];
}
if (!CallbackCasting::supports($accessor, $typeCaster)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}
return CallbackCasting::class.$typeCaster;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
final class MapCell
{
/**
* @param class-string|string|null $cast
*/
public function __construct(
public readonly string|int|null $column = null,
public readonly ?string $cast = null,
public readonly array $options = [],
public readonly bool $ignore = false,
public readonly ?bool $convertEmptyStringToNull = null,
public readonly ?bool $trimFieldValueBeforeCasting = false,
) {
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Attribute;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ValueError;
#[Attribute(Attribute::TARGET_CLASS)]
final class MapRecord
{
public function __construct(
/** @var array<string> $afterMapping */
public readonly array $afterMapping = [],
public readonly ?bool $convertEmptyStringToNull = null,
public readonly bool $trimFieldValueBeforeCasting = false,
) {
foreach ($this->afterMapping as $method) {
is_string($method) || throw new ValueError('The method names must be string.');
}
}
/**
* @return array<ReflectionMethod>
*/
public function afterMappingMethods(ReflectionClass $class): array
{
$methods = [];
foreach ($this->afterMapping as $method) {
try {
$accessor = $class->getMethod($method);
} catch (ReflectionException $exception) {
throw new MappingFailed('The method `'.$method.'` is not defined on the `'.$class->getName().'` class.', 0, $exception);
}
0 === $accessor->getNumberOfRequiredParameters() || throw new MappingFailed('The method `'.$class->getName().'::'.$accessor->getName().'` has too many required parameters.');
$methods[] = $accessor;
}
return $methods;
}
/**
* @throws MappingFailed
*/
public static function tryFrom(ReflectionClass $class): ?self
{
$attributes = $class->getAttributes(self::class, ReflectionAttribute::IS_INSTANCEOF);
$nbAttributes = count($attributes);
return match ($nbAttributes) {
0 => null,
1 => $attributes[0]->newInstance(),
default => throw new MappingFailed('Using more than one `'.self::class.'` attribute on a class property or method is not supported.'),
};
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use LogicException;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
final class MappingFailed extends LogicException implements SerializationFailed
{
public static function dueToUnsupportedType(ReflectionProperty|ReflectionParameter $reflectionProperty): self
{
$suffix = 'is missing; register it using the `'.Denormalizer::class.'` class.';
return new self(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The type definition for the method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` first argument `'.$reflectionProperty->getName().'` '.$suffix,
$reflectionProperty instanceof ReflectionProperty => 'The property type definition for `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` '.$suffix,
});
}
public static function dueToTypeCastingUnsupportedType(
ReflectionProperty|ReflectionParameter $reflectionProperty,
TypeCasting $typeCasting,
string ...$types
): self {
$suffix = 'is invalid; `'.implode('` or `', $types).'` type must be used with the `'.$typeCasting::class.'`.';
return new self(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The type for the method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` first argument `'.$reflectionProperty->getName().'` '.$suffix,
$reflectionProperty instanceof ReflectionProperty => 'The property type for `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` '.$suffix,
});
}
public static function dueToInvalidCastingArguments(?Throwable $exception = null): self
{
return new self('Unable to load the casting mechanism. Please verify your casting arguments', 0, $exception);
}
public static function dueToInvalidTypeCastingClass(string $typeCaster): self
{
return new self('`'.$typeCaster.'` must be an resolvable class implementing the `'.TypeCasting::class.'` interface or a supported alias.');
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
use function array_key_exists;
use function is_string;
/**
* @internal
*/
final class PropertySetter
{
public function __construct(
public readonly ReflectionMethod|ReflectionProperty $accessor,
public readonly int $offset,
public readonly TypeCasting $cast,
public readonly bool $convertEmptyStringToNull,
public readonly bool $trimFieldValueBeforeCasting,
) {
}
/**
* @throws ReflectionException
* @throws TypeCastingFailed
*/
public function __invoke(object $object, array $recordValues): void
{
$typeCastedValue = $this->cast->toVariable($this->getRecordValue($recordValues));
match (true) {
$this->accessor instanceof ReflectionMethod => $this->accessor->invoke($object, $typeCastedValue),
$this->accessor instanceof ReflectionProperty => $this->accessor->setValue($object, $typeCastedValue),
};
}
/**
* @throws TypeCastingFailed
*/
private function getRecordValue(array $record): mixed
{
if (!array_key_exists($this->offset, $record)) {
throw TypeCastingFailed::dueToUndefinedValue($this->offset, TypeCastingInfo::fromAccessor($this->accessor));
}
$value = $record[$this->offset];
if (is_string($value) && $this->trimFieldValueBeforeCasting) {
$value = trim($value);
}
if ('' === $value && $this->convertEmptyStringToNull) {
return null;
}
return $value;
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use Throwable;
interface SerializationFailed extends Throwable
{
}

View File

@ -0,0 +1,163 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Serializer;
use DateTimeInterface;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use Throwable;
use UnitEnum;
use function class_exists;
use function enum_exists;
use function in_array;
use const FILTER_UNSAFE_RAW;
use const FILTER_VALIDATE_BOOL;
use const FILTER_VALIDATE_FLOAT;
use const FILTER_VALIDATE_INT;
enum Type: string
{
case Bool = 'bool';
case True = 'true';
case False = 'false';
case Null = 'null';
case Int = 'int';
case Float = 'float';
case String = 'string';
case Mixed = 'mixed';
case Array = 'array';
case Iterable = 'iterable';
case Enum = UnitEnum::class;
case Date = DateTimeInterface::class;
public function equals(mixed $value): bool
{
return $value instanceof self
&& $value === $this;
}
public function isOneOf(self ...$types): bool
{
return in_array($this, $types, true);
}
public function filterFlag(): int
{
return match ($this) {
self::Bool,
self::True,
self::False => FILTER_VALIDATE_BOOL,
self::Int => FILTER_VALIDATE_INT,
self::Float => FILTER_VALIDATE_FLOAT,
default => FILTER_UNSAFE_RAW,
};
}
public static function resolve(ReflectionProperty|ReflectionParameter $reflectionProperty, array $arguments = []): TypeCasting
{
try {
$cast = match (self::tryFromAccessor($reflectionProperty)) {
self::Mixed, self::Null, self::String => new CastToString($reflectionProperty),
self::Iterable, self::Array => new CastToArray($reflectionProperty),
self::False, self::True, self::Bool => new CastToBool($reflectionProperty),
self::Float => new CastToFloat($reflectionProperty),
self::Int => new CastToInt($reflectionProperty),
self::Date => new CastToDate($reflectionProperty),
self::Enum => new CastToEnum($reflectionProperty),
null => throw MappingFailed::dueToUnsupportedType($reflectionProperty),
};
$cast->setOptions(...$arguments);
return $cast;
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
/**
* @return list<array{0:Type, 1: ReflectionNamedType}>
*/
public static function list(ReflectionParameter|ReflectionProperty $reflectionProperty): array
{
$reflectionType = $reflectionProperty->getType() ?? throw MappingFailed::dueToUnsupportedType($reflectionProperty);
$foundTypes = static function (array $res, ReflectionType $reflectionType) {
if (!$reflectionType instanceof ReflectionNamedType) {
return $res;
}
$type = self::tryFromName($reflectionType->getName());
if (null !== $type) {
$res[] = [$type, $reflectionType];
}
return $res;
};
return match (true) {
$reflectionType instanceof ReflectionNamedType => $foundTypes([], $reflectionType),
$reflectionType instanceof ReflectionUnionType => array_reduce($reflectionType->getTypes(), $foundTypes, []),
default => [],
};
}
public static function tryFromName(string $propertyType): ?self
{
$interfaceExists = interface_exists($propertyType);
return match (true) {
enum_exists($propertyType),
$interfaceExists && (new ReflectionClass($propertyType))->implementsInterface(UnitEnum::class) => self::Enum,
$interfaceExists && (new ReflectionClass($propertyType))->implementsInterface(DateTimeInterface::class),
class_exists($propertyType) && (new ReflectionClass($propertyType))->implementsInterface(DateTimeInterface::class) => self::Date,
default => self::tryFrom($propertyType),
};
}
public static function tryFromAccessor(ReflectionProperty|ReflectionParameter $reflectionProperty): ?self
{
$type = $reflectionProperty->getType();
if (null === $type) {
return Type::Mixed;
}
if ($type instanceof ReflectionNamedType) {
return self::tryFromName($type->getName());
}
if (!$type instanceof ReflectionUnionType) {
return null;
}
foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof ReflectionNamedType) {
continue;
}
$result = self::tryFromName($innerType->getName());
if ($result instanceof self) {
return $result;
}
}
return null;
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
/**
* @template TValue
*
* @method TypeCastingInfo info() the name of the property the value will be assigned to
*/
interface TypeCasting
{
/**
* @throws TypeCastingFailed
*
* @return TValue
*/
public function toVariable(mixed $value): mixed;
/**
* Accepts additional parameters to configure the class
* Parameters should be scalar value, null or array containing
* only scalar value and null.
*
* @throws MappingFailed
*/
public function setOptions(): void;
}

View File

@ -0,0 +1,74 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use RuntimeException;
use Throwable;
use function is_int;
final class TypeCastingFailed extends RuntimeException implements SerializationFailed
{
public readonly ?TypeCastingInfo $info;
public function __construct(string $message, int $code = 0, ?Throwable $previous = null, ?TypeCastingInfo $info = null)
{
parent::__construct(self::format($message, $info), $code, $previous);
$this->info = $info;
}
private static function format(string $message, ?TypeCastingInfo $info = null): string
{
if (null === $info) {
return $message;
}
$className = $info->targetClassName;
if (null !== $className) {
$className .= '::';
}
$target = $info->targetName;
$target = (TypeCastingTargetType::MethodFirstArgument === $info->targetType)
? 'the first argument `'.$target.'` of the method `'.$className.$info->targetMethodName.'()`'
: 'the property `'.$className.$target.'`';
$source = $info->source;
$source = is_int($source)
? "the record field offset `$source`"
: "the record field `$source`";
return "Casting $target using $source failed; $message";
}
public static function dueToNotNullableType(string $type, ?Throwable $exception = null, ?TypeCastingInfo $info = null): self
{
return new self('The `null` value can not be cast to a `'.$type.'`; the property type is not nullable.', 0, $exception, $info);
}
public static function dueToInvalidValue(mixed $value, string $type, ?Throwable $previous = null, ?TypeCastingInfo $info = null): self
{
if (!is_scalar($value)) {
$value = gettype($value);
}
return new self('Unable to cast the given data `'.$value.'` to a `'.$type.'`.', 0, $previous, $info);
}
public static function dueToUndefinedValue(string|int $offset, ?TypeCastingInfo $info = null): self
{
return new self('Unable to cast the record value; Missing value was for offset `'.$offset.'`.', 0, info: $info);
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv\Serializer;
use ReflectionAttribute;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use ValueError;
use function strtolower;
use function substr;
final class TypeCastingInfo
{
public function __construct(
public readonly int|string $source,
public readonly TypeCastingTargetType $targetType,
public readonly string $targetName,
public readonly ?string $targetMethodName,
public readonly ?string $targetClassName,
) {
}
public static function fromAccessor(ReflectionMethod|ReflectionProperty|ReflectionParameter $accessor): self
{
return match (true) {
$accessor instanceof ReflectionMethod => self::fromMethod($accessor),
$accessor instanceof ReflectionParameter => self::fromMethodFirstArgument($accessor),
$accessor instanceof ReflectionProperty => self::fromProperty($accessor),
};
}
public static function fromMethod(ReflectionMethod $accessor): self
{
$accessor = $accessor->getParameters()[0] ?? null;
if (null === $accessor) {
throw new ValueError('The method must contain at least one parameter in its signature.');
}
return self::fromMethodFirstArgument($accessor);
}
public static function fromMethodFirstArgument(ReflectionParameter $accessor): self
{
/** @var ReflectionMethod $method */
$method = $accessor->getDeclaringFunction();
$className = $method->getDeclaringClass()->getName();
return new self(
self::resolveSource($method),
TypeCastingTargetType::MethodFirstArgument,
$accessor->getName(),
$method->getName(),
$className,
);
}
public static function fromProperty(ReflectionProperty $accessor): self
{
$attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF);
$source = [] === $attributes ? $accessor->getName() : ($attributes[0]->newInstance()->column ?? $accessor->getName());
$className = $accessor->getDeclaringClass()->getName();
return new self(
$source,
TypeCastingTargetType::PropertyName,
$accessor->getName(),
null,
$className,
);
}
private static function resolveSource(ReflectionFunctionAbstract $method): int|string
{
$attributes = $method->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF);
if ([] === $attributes) {
return self::getColumnName($method);
}
$name = $attributes[0]->newInstance()->column;
if (null !== $name) {
return $name;
}
return self::getColumnName($method);
}
private static function getColumnName(ReflectionFunctionAbstract $method): string
{
$name = $method->getName();
if (!str_starts_with($name, 'set')) {
throw new ValueError('The method `'.$name.'` has no Mapping information and does not start with `set`.');
}
return strtolower($name[3]).substr($name, 4);
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Serializer;
use ReflectionParameter;
use ReflectionProperty;
enum TypeCastingTargetType
{
case PropertyName;
case MethodFirstArgument;
public static function fromAccessor(ReflectionParameter|ReflectionProperty $accessor): self
{
if ($accessor instanceof ReflectionProperty) {
return self::PropertyName;
}
return self::MethodFirstArgument;
}
}

518
vendor/league/csv/src/Statement.php vendored Normal file
View File

@ -0,0 +1,518 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Deprecated;
use Iterator;
use OutOfBoundsException;
use ReflectionException;
use ReflectionFunction;
use function array_key_exists;
use function array_reduce;
use function array_search;
use function array_values;
use function is_string;
/**
* Criteria to filter a {@link TabularData} object.
*
* @phpstan-import-type ConditionExtended from Query\PredicateCombinator
* @phpstan-import-type OrderingExtended from Query\SortCombinator
*/
class Statement
{
final protected const COLUMN_ALL = 0;
final protected const COLUMN_INCLUDE = 1;
final protected const COLUMN_EXCLUDE = 2;
/** @var array<ConditionExtended> Callables to filter the iterator. */
protected array $where = [];
/** @var array<OrderingExtended> Callables to sort the iterator. */
protected array $order_by = [];
/** iterator Offset. */
protected int $offset = 0;
/** iterator maximum length. */
protected int $limit = -1;
/** @var array<string|int> */
protected array $select = [];
/** @var self::COLUMN_* */
protected int $select_mode = self::COLUMN_ALL;
/**
* Select all the columns from the tabular data that MUST BE present in the ResultSet.
*/
public function select(string|int ...$columns): self
{
if ($columns === $this->select && self::COLUMN_INCLUDE === $this->select_mode) {
return $this;
}
$clone = clone $this;
$clone->select = $columns;
$clone->select_mode = [] === $columns ? self::COLUMN_ALL : self::COLUMN_INCLUDE;
return $clone;
}
/**
* Select all the columns from the tabular data that MUST NOT BE present in the ResultSet.
*/
public function selectAllExcept(string|int ...$columns): self
{
if ($columns === $this->select && self::COLUMN_EXCLUDE === $this->select_mode) {
return $this;
}
$clone = clone $this;
$clone->select = $columns;
$clone->select_mode = [] === $columns ? self::COLUMN_ALL : self::COLUMN_EXCLUDE;
return $clone;
}
/**
* Sets the Iterator filter method.
*
* @param callable(array, array-key): bool $where
*
* @throws ReflectionException
* @throws InvalidArgument
*/
public function where(callable $where): self
{
$where = self::wrapSingleArgumentCallable($where);
$clone = clone $this;
$clone->where[] = $where;
return $clone;
}
/**
* Sanitize the number of required parameters for a predicate.
*
* To avoid BC break in 9.16+ version the predicate should have
* at least 1 required argument.
*
* @throws InvalidArgument
* @throws ReflectionException
*
* @return ConditionExtended
*/
final protected static function wrapSingleArgumentCallable(callable $where): callable
{
if ($where instanceof Query\Predicate) {
return $where;
}
$reflection = new ReflectionFunction($where instanceof Closure ? $where : $where(...));
return match ($reflection->getNumberOfRequiredParameters()) {
0 => throw new InvalidArgument('The where condition must be callable with 2 required parameters.'),
1 => fn (mixed $record, int $key) => $where($record),
default => $where,
};
}
public function andWhere(string|int $column, Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('and', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function orWhere(string|int $column, Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('or', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function whereNot(string|int $column, Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('not', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function xorWhere(string|int $column, Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('xor', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function andWhereColumn(string|int $first, Query\Constraint\Comparison|callable|string $operator, array|int|string $second): self
{
return $this->appendWhere('and', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function orWhereColumn(string|int $first, Query\Constraint\Comparison|callable|string $operator, array|int|string $second): self
{
return $this->appendWhere('or', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function xorWhereColumn(string|int $first, Query\Constraint\Comparison|callable|string $operator, array|int|string $second): self
{
return $this->appendWhere('xor', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function whereNotColumn(string|int $first, Query\Constraint\Comparison|callable|string $operator, array|int|string $second): self
{
return $this->appendWhere('not', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function andWhereOffset(Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('and', Query\Constraint\Offset::filterOn($operator, $value));
}
public function orWhereOffset(Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('or', Query\Constraint\Offset::filterOn($operator, $value));
}
public function xorWhereOffset(Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('xor', Query\Constraint\Offset::filterOn($operator, $value));
}
public function whereNotOffset(Query\Constraint\Comparison|Closure|callable|string $operator, mixed $value = null): self
{
return $this->appendWhere('not', Query\Constraint\Offset::filterOn($operator, $value));
}
/**
* @param 'and'|'not'|'or'|'xor' $joiner
*/
final protected function appendWhere(string $joiner, Query\Predicate $predicate): self
{
if ([] === $this->where) {
return $this->where(match ($joiner) {
'and' => $predicate,
'not' => Query\Constraint\Criteria::none($predicate),
'or' => Query\Constraint\Criteria::any($predicate),
'xor' => Query\Constraint\Criteria::xany($predicate),
});
}
$predicates = Query\Constraint\Criteria::all(...$this->where);
$clone = clone $this;
$clone->where = [match ($joiner) {
'and' => $predicates->and($predicate),
'not' => $predicates->not($predicate),
'or' => $predicates->or($predicate),
'xor' => $predicates->xor($predicate),
}];
return $clone;
}
/**
* Sets an Iterator sorting callable function.
*
* @param OrderingExtended $order_by
*/
public function orderBy(callable|Query\Sort|Closure $order_by): self
{
$clone = clone $this;
$clone->order_by[] = $order_by;
return $clone;
}
/**
* Ascending ordering of the tabular data according to a column value.
*
* The column value can be modified using the callback before ordering.
*/
public function orderByAsc(string|int $column, callable|Closure|null $callback = null): self
{
return $this->orderBy(Query\Ordering\Column::sortOn($column, 'asc', $callback));
}
/**
* Descending ordering of the tabular data according to a column value.
*
* The column value can be modified using the callback before ordering.
*/
public function orderByDesc(string|int $column, callable|Closure|null $callback = null): self
{
return $this->orderBy(Query\Ordering\Column::sortOn($column, 'desc', $callback));
}
/**
* Sets LimitIterator Offset.
*
* @throws Exception if the offset is less than 0
*/
public function offset(int $offset): self
{
if (0 > $offset) {
throw InvalidArgument::dueToInvalidRecordOffset($offset, __METHOD__);
}
if ($offset === $this->offset) {
return $this;
}
$clone = clone $this;
$clone->offset = $offset;
return $clone;
}
/**
* Sets LimitIterator Count.
*
* @throws Exception if the limit is less than -1
*/
public function limit(int $limit): self
{
$limit >= -1 || throw InvalidArgument::dueToInvalidLimit($limit, __METHOD__);
if ($limit === $this->limit) {
return $this;
}
$clone = clone $this;
$clone->limit = $limit;
return $clone;
}
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* Executes the prepared Statement on the {@link TabularData} object.
*
* @param array<string> $header an optional header to use instead of the tabular data header
*
* @throws InvalidArgument
* @throws SyntaxError
*/
public function process(TabularData $tabular_data, array $header = []): TabularDataReader
{
if (!$tabular_data instanceof TabularDataReader) {
$tabular_data = ResultSet::from($tabular_data);
}
if ([] === $header) {
$header = $tabular_data->getHeader();
}
$iterator = $tabular_data->getRecords($header);
if ([] !== $this->where) {
$iterator = Query\Constraint\Criteria::all(...$this->where)->filter($iterator);
}
if ([] !== $this->order_by) {
$iterator = Query\Ordering\MultiSort::all(...$this->order_by)->sort($iterator);
}
if (0 !== $this->offset || -1 !== $this->limit) {
$iterator = Query\Limit::new($this->offset, $this->limit)->slice($iterator);
}
$iterator = new ResultSet($iterator, $header);
return match ($this->select_mode) {
self::COLUMN_EXCLUDE => $iterator->selectAllExcept(...$this->select),
self::COLUMN_INCLUDE => $iterator->select(...$this->select),
default => $iterator,
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws InvalidArgument
*
* @throws SyntaxError
* @see Statement::process()
* @deprecated Since version 9.16.0
*/
#[Deprecated(message:'this method no longer affects on the outcome of the class, use League\Csv\Statement::process() instead', since:'league/csv:9.16.0')]
protected function applySelect(Iterator $records, array $recordsHeader, array $select): TabularDataReader
{
$hasHeader = [] !== $recordsHeader;
$selectColumn = function (array $header, string|int $field) use ($recordsHeader, $hasHeader): array {
if (is_string($field)) {
$index = array_search($field, $recordsHeader, true);
if (false === $index) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$index] = $field;
return $header;
}
if ($hasHeader && !array_key_exists($field, $recordsHeader)) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$field] = $recordsHeader[$field] ?? $field;
return $header;
};
/** @var array<string> $header */
$header = array_reduce($select, $selectColumn, []);
$callback = function (array $record) use ($header): array {
$element = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$element[$headerName] = $row[$offset] ?? null;
}
return $element;
};
return new ResultSet(new MapIterator($records, $callback), $hasHeader ? $header : []);
}
/**
* Filters elements of an Iterator using a callback function.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::applyFilter()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'this method no longer affects on the outcome of the class, use League\Csv\Statement::applyFilter() instead', since:'league/csv:9.15.0')]
protected function filter(Iterator $iterator, callable $callable): CallbackFilterIterator
{
return new CallbackFilterIterator($iterator, $callable);
}
/**
* Filters elements of an Iterator using a callback function.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::process()
* @deprecated Since version 9.16.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'this method no longer affects on the outcome of the class, use League\Csv\Statement::process() instead', since:'league/csv:9.16.0')]
protected function applyFilter(Iterator $iterator): Iterator
{
$filter = function (array $record, string|int $key): bool {
foreach ($this->where as $where) {
if (true !== $where($record, $key)) {
return false;
}
}
return true;
};
return new CallbackFilterIterator($iterator, $filter);
}
/**
* Sorts the Iterator.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::process()
* @deprecated Since version 9.16.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'this method no longer affects on the outcome of the class, use League\Csv\Statement::process() instead', since:'league/csv:9.16.0')]
protected function buildOrderBy(Iterator $iterator): Iterator
{
if ([] === $this->order_by) {
return $iterator;
}
$compare = function (array $record_a, array $record_b): int {
foreach ($this->order_by as $callable) {
if (0 !== ($cmp = $callable($record_a, $record_b))) {
return $cmp;
}
}
return $cmp ?? 0;
};
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
/** @var ArrayIterator<array-key, array<string|null>> $it */
$it = new $class([...$iterator]);
$it->uasort($compare);
return $it;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @param ?callable(array, array-key): bool $where , Deprecated argument use Statement::where instead
* @param int $offset, Deprecated argument use Statement::offset instead
* @param int $limit, Deprecated argument use Statement::limit instead
*
* @throws Exception
* @throws InvalidArgument
* @throws ReflectionException
*
* @see Statement::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\Statement::__construct() instead', since:'league/csv:9.22.0')]
public static function create(?callable $where = null, int $offset = 0, int $limit = -1): self
{
$stmt = new self();
if (null !== $where) {
$stmt = $stmt->where($where);
}
if (0 !== $offset) {
$stmt = $stmt->offset($offset);
}
if (-1 !== $limit) {
$stmt = $stmt->limit($limit);
}
return $stmt;
}
}

542
vendor/league/csv/src/Stream.php vendored Normal file
View File

@ -0,0 +1,542 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use RuntimeException;
use SeekableIterator;
use SplFileObject;
use Stringable;
use TypeError;
use ValueError;
use function array_keys;
use function array_walk_recursive;
use function fclose;
use function feof;
use function fflush;
use function fgetcsv;
use function fopen;
use function fpassthru;
use function fputcsv;
use function fread;
use function fseek;
use function fwrite;
use function get_resource_type;
use function gettype;
use function is_array;
use function is_resource;
use function restore_error_handler;
use function rewind;
use function set_error_handler;
use function stream_filter_append;
use function stream_filter_remove;
use function stream_get_meta_data;
use function strlen;
use const SEEK_SET;
/**
* An object-oriented API to handle a PHP stream resource.
*
* @internal used internally to iterate over a stream resource
*/
final class Stream implements SeekableIterator
{
/** @var resource */
private $stream;
private bool $is_seekable;
private bool $should_close_stream = false;
/** @var mixed can be a null, false or a scalar type value. Current iterator value. */
private mixed $value = null;
/** Current iterator key. */
private int $offset = -1;
/** Flags for the Document. */
private int $flags = 0;
private string $delimiter = ',';
private string $enclosure = '"';
private string $escape = '\\';
/** @var array<string, array<resource>> Attached filters. */
private array $filters = [];
private int $maxLength = 0;
/**
* @param resource $stream stream type resource
*/
private function __construct($stream)
{
$this->is_seekable = stream_get_meta_data($stream)['seekable'];
$this->stream = $stream;
}
public function __destruct()
{
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
array_walk_recursive($this->filters, static function ($filter): void {
if (is_resource($filter)) {
stream_filter_remove($filter);
}
});
if ($this->should_close_stream && is_resource($this->stream)) {
fclose($this->stream);
}
restore_error_handler();
unset($this->stream);
}
public function __clone(): void
{
throw UnavailableStream::dueToForbiddenCloning(self::class);
}
public function __debugInfo(): array
{
return stream_get_meta_data($this->stream) + [
'delimiter' => $this->delimiter,
'enclosure' => $this->enclosure,
'escape' => $this->escape,
'stream_filters' => array_keys($this->filters),
];
}
/**
* Returns the actual mode used to open the resource stream.
*/
public function getMode(): string
{
return stream_get_meta_data($this->stream)['mode'];
}
public function ftell(): int|false
{
return ftell($this->stream);
}
/**
* Returns a new instance from a file path.
*
* @param resource|null $context
*
* @throws UnavailableStream if the stream resource can not be created
*/
public static function createFromPath(string $path, string $open_mode = 'r', $context = null): self
{
$args = [$path, $open_mode];
if (null !== $context) {
$args[] = false;
$args[] = $context;
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$resource = fopen(...$args);
restore_error_handler();
is_resource($resource) || throw UnavailableStream::dueToPathNotFound($path);
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* Returns a new instance from a string.
*/
public static function createFromString(Stringable|string $content = ''): self
{
/** @var resource $resource */
$resource = fopen('php://temp', 'r+');
fwrite($resource, (string) $content);
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
public static function createFromResource(mixed $stream): self
{
return match (true) {
!is_resource($stream) => throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.'),
'stream' !== ($type = get_resource_type($stream)) => throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given'),
default => new self($stream),
};
}
/**
* Returns the URI of the underlying stream.
*
* @see https://www.php.net/manual/en/splfileinfo.getpathname.php
*/
public function getPathname(): string
{
return stream_get_meta_data($this->stream)['uri'];
}
/**
* Appends a filter.
*
* @see http://php.net/manual/en/function.stream-filter-append.php
*
* @throws InvalidArgument if the filter can not be appended
*/
public function appendFilter(string $filtername, int $read_write, mixed $params = null): void
{
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$res = stream_filter_append($this->stream, $filtername, $read_write, $params);
restore_error_handler();
is_resource($res) || throw InvalidArgument::dueToStreamFilterNotFound($filtername);
$this->filters[$filtername][] = $res;
}
/**
* Appends a filter.
*
* @see http://php.net/manual/en/function.stream-filter-append.php
*
* @throws InvalidArgument if the filter can not be appended
*/
public function prependFilter(string $filtername, int $read_write, mixed $params = null): void
{
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$res = stream_filter_prepend($this->stream, $filtername, $read_write, $params);
restore_error_handler();
is_resource($res) || throw InvalidArgument::dueToStreamFilterNotFound($filtername);
$this->filters[$filtername][] = $res;
}
/**
* Sets CSV control.
*
* @see https://www.php.net/manual/en/splfileobject.setcsvcontrol.php
*
* @throws InvalidArgument
*/
public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\'): void
{
[$this->delimiter, $this->enclosure, $this->escape] = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
}
/**
* Filters CSV control characters.
*
* @throws InvalidArgument If the CSV control character is not exactly one character.
*
* @return array{0:string, 1:string, 2:string}
*/
private function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array
{
return match (true) {
1 !== strlen($delimiter) => throw InvalidArgument::dueToInvalidDelimiterCharacter($delimiter, $caller),
1 !== strlen($enclosure) => throw InvalidArgument::dueToInvalidEnclosureCharacter($enclosure, $caller),
1 !== strlen($escape) && '' !== $escape => throw InvalidArgument::dueToInvalidEscapeCharacter($escape, $caller),
default => [$delimiter, $enclosure, $escape],
};
}
/**
* Returns CSV control.
*
* @see https://www.php.net/manual/en/splfileobject.getcsvcontrol.php
*
* @return array<string>
*/
public function getCsvControl(): array
{
return [$this->delimiter, $this->enclosure, $this->escape];
}
/**
* Sets CSV stream flags.
*
* @see https://www.php.net/manual/en/splfileobject.setflags.php
*/
public function setFlags(int $flags): void
{
$this->flags = $flags;
}
/**
* Writes a field array as a CSV line.
*
* @see https://www.php.net/manual/en/splfileobject.fputcsv.php
*
* @throws InvalidArgument If the CSV control character is not exactly one character.
*/
public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\', string $eol = "\n"): int|false
{
return fputcsv(
$this->stream,
$fields,
...[...$this->filterControl($delimiter, $enclosure, $escape, __METHOD__), $eol]
);
}
/**
* Gets line number.
*
* @see https://www.php.net/manual/en/splfileobject.key.php
*/
public function key(): int
{
return $this->offset;
}
/**
* Reads next line.
*
* @see https://www.php.net/manual/en/splfileobject.next.php
*/
public function next(): void
{
$this->value = false;
$this->offset++;
}
/**
* Rewinds the file to the first line.
*
* @see https://www.php.net/manual/en/splfileobject.rewind.php
*
* @throws Exception if the stream resource is not seekable
* @throws RuntimeException if rewinding the stream fails.
*/
public function rewind(): void
{
$this->is_seekable || throw UnavailableFeature::dueToMissingStreamSeekability();
false !== rewind($this->stream) || throw new RuntimeException('Unable to rewind the document.');
$this->offset = 0;
$this->value = false;
if (SplFileObject::READ_AHEAD === ($this->flags & SplFileObject::READ_AHEAD)) {
$this->current();
}
}
/**
* Not at EOF.
*
* @see https://www.php.net/manual/en/splfileobject.valid.php
*/
public function valid(): bool
{
return match (true) {
SplFileObject::READ_AHEAD === ($this->flags & SplFileObject::READ_AHEAD) => false !== $this->current(),
default => !feof($this->stream),
};
}
/**
* Retrieves the current line of the file.
*
* @see https://www.php.net/manual/en/splfileobject.current.php
*/
public function current(): mixed
{
if (false !== $this->value) {
return $this->value;
}
$this->value = match (true) {
SplFileObject::READ_CSV === ($this->flags & SplFileObject::READ_CSV) => $this->getCurrentRecord(),
default => $this->getCurrentLine(),
};
return $this->value;
}
public function fgets(): string|false
{
$arg = [$this->stream];
if (0 < $this->maxLength) {
$arg[] = $this->maxLength;
}
return fgets(...$arg);
}
/**
* Sets the maximum length of a line to be read.
*
* @see https://www.php.net/manual/en/splfileobject.setmaxlinelen.php
*/
public function setMaxLineLen(int $maxLength): void
{
0 <= $maxLength || throw new ValueError(' Argument #1 ($maxLength) must be greater than or equal to 0');
$this->maxLength = $maxLength;
}
/**
* Gets the maximum line length as set by setMaxLineLen.
*
* @see https://www.php.net/manual/en/splfileobject.getmaxlinelen.php
*/
public function getMaxLineLen(): int
{
return $this->maxLength;
}
/**
* Tells whether the end of file has been reached.
*
* @see https://www.php.net/manual/en/splfileobject.eof.php
*/
public function eof(): bool
{
return feof($this->stream);
}
/**
* Retrieves the current line as a CSV Record.
*/
private function getCurrentRecord(): array|false
{
$isEmptyLine = SplFileObject::SKIP_EMPTY === ($this->flags & SplFileObject::SKIP_EMPTY);
do {
$ret = fgetcsv($this->stream, 0, $this->delimiter, $this->enclosure, $this->escape);
} while ($isEmptyLine && is_array($ret) && null === $ret[0]);
return $ret;
}
/**
* Retrieves the current line.
*/
private function getCurrentLine(): string|false
{
$isEmptyLine = SplFileObject::SKIP_EMPTY === ($this->flags & SplFileObject::SKIP_EMPTY);
$dropNewLine = SplFileObject::DROP_NEW_LINE === ($this->flags & SplFileObject::DROP_NEW_LINE);
$shouldBeIgnored = fn (string|false $line): bool => ($isEmptyLine || $dropNewLine)
&& (false !== $line && '' === rtrim($line, "\r\n"));
$arguments = [$this->stream];
if (0 < $this->maxLength) {
$arguments[] = $this->maxLength;
}
do {
$line = fgets(...$arguments);
} while ($shouldBeIgnored($line));
if ($dropNewLine && false !== $line) {
return rtrim($line, "\r\n");
}
return $line;
}
/**
* Seeks to specified line.
*
* @see https://www.php.net/manual/en/splfileobject.seek.php
*
* @throws Exception if the position is negative
*/
public function seek(int $offset): void
{
$offset >= 0 || throw InvalidArgument::dueToInvalidSeekingPosition($offset, __METHOD__);
$this->rewind();
while ($this->key() !== $offset && $this->valid()) {
$this->current();
$this->next();
}
if (0 !== $offset) {
$this->offset--;
}
$this->current();
}
/**
* Outputs all remaining data on a file pointer.
*
* @see https://www.php.net/manual/en/splfileobject.fpassthru.php
*/
public function fpassthru(): int|false
{
return fpassthru($this->stream);
}
/**
* Reads from file.
*
* @see https://www.php.net/manual/en/splfileobject.fread.php
*
* @param int<1, max> $length The number of bytes to read
*/
public function fread(int $length): string|false
{
return fread($this->stream, $length);
}
/**
* Seeks to a position.
*
* @see https://www.php.net/manual/en/splfileobject.fseek.php
*
* @throws Exception if the stream resource is not seekable
*/
public function fseek(int $offset, int $whence = SEEK_SET): int
{
return match (true) {
!$this->is_seekable => throw UnavailableFeature::dueToMissingStreamSeekability(),
default => fseek($this->stream, $offset, $whence),
};
}
/**
* Write to stream.
*
* @see http://php.net/manual/en/SplFileObject.fwrite.php
*/
public function fwrite(string $str, ?int $length = null): int|false
{
$args = [$this->stream, $str];
if (null !== $length) {
$args[] = $length;
}
return fwrite(...$args);
}
/**
* Flushes the output to a file.
*
* @see https://www.php.net/manual/en/splfileobject.fflush.php
*/
public function fflush(): bool
{
return fflush($this->stream);
}
/**
* Gets file size.
*
* @see https://www.php.net/manual/en/splfileinfo.getsize.php
*/
public function getSize(): int|false
{
return fstat($this->stream)['size'] ?? false;
}
public function getContents(?int $length = null, int $offset = -1): string|false
{
return stream_get_contents($this->stream, $length, $offset);
}
}

192
vendor/league/csv/src/StreamFilter.php vendored Normal file
View File

@ -0,0 +1,192 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use LogicException;
use RuntimeException;
use TypeError;
use function get_resource_type;
use function gettype;
use function in_array;
use function is_resource;
use function restore_error_handler;
use function set_error_handler;
use function stream_filter_append;
use function stream_get_filters;
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
final class StreamFilter
{
/**
* Remove a filter from a stream.
*
* @param resource $stream_filter
*/
public static function remove($stream_filter): bool
{
if (!is_resource($stream_filter)) {
throw new TypeError('Argument passed must be a stream resource, '.gettype($stream_filter).' given.');
}
if ('stream filter' !== ($type = get_resource_type($stream_filter))) {
throw new TypeError('Argument passed must be a stream filter resource, '.$type.' resource given');
}
return stream_filter_remove($stream_filter);
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
public static function appendOnReadTo(mixed $stream, string $filtername, mixed $params = null): mixed
{
return self::appendFilter(STREAM_FILTER_READ, $stream, $filtername, $params);
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
public static function appendOnWriteTo(mixed $stream, string $filtername, mixed $params = null): mixed
{
return self::appendFilter(STREAM_FILTER_WRITE, $stream, $filtername, $params);
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
public static function prependOnReadTo(mixed $stream, string $filtername, mixed $params = null): mixed
{
return self::prependFilter(STREAM_FILTER_READ, $stream, $filtername, $params);
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
public static function prependOnWriteTo(mixed $stream, string $filtername, mixed $params = null): mixed
{
return self::prependFilter(STREAM_FILTER_WRITE, $stream, $filtername, $params);
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
private static function prependFilter(int $mode, mixed $stream, string $filtername, mixed $params): mixed
{
self::filterFiltername($filtername);
self::filterStream($stream);
if ($stream instanceof AbstractCsv) {
return match ($mode) {
STREAM_FILTER_WRITE => $stream->prependStreamFilterOnWrite($filtername, $params),
default => $stream->prependStreamFilterOnRead($filtername, $params),
};
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_prepend($stream, $filtername, $mode, $params);
restore_error_handler();
if (!is_resource($filter)) {
throw new RuntimeException('Could not append the registered stream filter: '.$filtername);
}
return $filter;
}
/**
* @param resource|AbstractCsv $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource|AbstractCsv
*/
private static function appendFilter(int $mode, mixed $stream, string $filtername, mixed $params): mixed
{
self::filterFiltername($filtername);
self::filterStream($stream);
if ($stream instanceof AbstractCsv) {
return match ($mode) {
STREAM_FILTER_WRITE => $stream->appendStreamFilterOnWrite($filtername, $params),
default => $stream->appendStreamFilterOnRead($filtername, $params),
};
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_append($stream, $filtername, $mode, $params);
restore_error_handler();
if (!is_resource($filter)) {
throw new RuntimeException('Could not append the registered stream filter: '.$filtername);
}
return $filter;
}
private static function filterFiltername(string $filtername): void
{
if (!in_array($filtername, stream_get_filters(), true)) {
throw new LogicException('The stream filter "'.$filtername.'" is not registered.');
}
}
/**
* Validate the resource given.
*
* @throws TypeError if the resource given is not a stream
*/
private static function filterStream(mixed $stream): void
{
if ($stream instanceof AbstractCsv) {
return;
}
if (!is_resource($stream)) {
throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.');
}
if ('stream' !== ($type = get_resource_type($stream))) {
throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given');
}
}
}

165
vendor/league/csv/src/SwapDelimiter.php vendored Normal file
View File

@ -0,0 +1,165 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use php_user_filter;
use RuntimeException;
use TypeError;
use function in_array;
use function str_replace;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_filter_register;
use function stream_get_filters;
use const PSFS_PASS_ON;
final class SwapDelimiter extends php_user_filter
{
private const FILTER_NAME = 'string.league.csv.delimiter';
public const MODE_READ = 'read';
public const MODE_WRITE = 'write';
private string $search = '';
private string $replace = '';
public static function getFiltername(): string
{
return self::FILTER_NAME;
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
in_array(self::FILTER_NAME, stream_get_filters(), true) || stream_filter_register(self::FILTER_NAME, self::class);
}
/**
* Static method to attach the stream filter to a CSV Reader or Writer instance.
*/
public static function addTo(AbstractCsv $csv, string $inputDelimiter): void
{
self::register();
if ($csv instanceof Reader) {
$csv->appendStreamFilterOnRead(self::getFiltername(), [
'mb_separator' => $inputDelimiter,
'separator' => $csv->getDelimiter(),
'mode' => self::MODE_READ,
]);
return;
}
$csv->appendStreamFilterOnWrite(self::getFiltername(), [
'mb_separator' => $inputDelimiter,
'separator' => $csv->getDelimiter(),
'mode' => self::MODE_WRITE,
]);
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function appendTo(mixed $stream, string $inputDelimiter, string $delimiter): mixed
{
self::register();
is_resource($stream) || throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.');
'stream' === ($type = get_resource_type($stream)) || throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given');
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_append($stream, self::getFiltername(), params: [
'mb_separator' => $inputDelimiter,
'separator' => $delimiter,
'mode' => str_contains(stream_get_meta_data($stream)['mode'], 'r') ? self::MODE_READ : self::MODE_WRITE,
]);
restore_error_handler();
is_resource($filter) || throw new RuntimeException('Could not append the registered stream filter: '.self::getFiltername());
return $filter;
}
/**
* @param resource $stream
*
* @throws TypeError
* @throws RuntimeException
*
* @return resource
*/
public static function prependTo(mixed $stream, string $inputDelimiter, string $delimiter): mixed
{
self::register();
is_resource($stream) || throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.');
'stream' === ($type = get_resource_type($stream)) || throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given');
$filtername = self::getFiltername();
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$filter = stream_filter_append($stream, $filtername, params: [
'mb_separator' => $inputDelimiter,
'separator' => $delimiter,
'mode' => str_contains(stream_get_meta_data($stream)['mode'], 'r') ? self::MODE_READ : self::MODE_WRITE,
]);
restore_error_handler();
is_resource($filter) || throw new RuntimeException('Could not prepend the registered stream filter: '.$filtername);
return $filter;
}
public function onCreate(): bool
{
if (self::FILTER_NAME !== $this->filtername) {
return false;
}
if (!is_array($this->params)) {
return false;
}
$mode = $this->params['mode'] ?? '';
[$this->search, $this->replace] = match ($mode) {
self::MODE_READ => [trim($this->params['mb_separator'] ?? ''), trim($this->params['separator'] ?? '')],
self::MODE_WRITE => [trim($this->params['separator'] ?? ''), trim($this->params['mb_separator'] ?? '')],
default => ['', ''],
};
return !in_array('', [$this->replace, $this->search], true);
}
public function filter($in, $out, &$consumed, bool $closing): int
{
$data = '';
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$data .= $bucket->data;
$consumed += $bucket->datalen;
}
$data = str_replace($this->search, $this->replace, $data);
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
stream_bucket_append($out, stream_bucket_new($this->stream, $data));
restore_error_handler();
return PSFS_PASS_ON;
}
}

65
vendor/league/csv/src/SyntaxError.php vendored Normal file
View File

@ -0,0 +1,65 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use Throwable;
use function array_count_values;
use function array_filter;
use function array_keys;
/**
* SyntaxError Exception.
*/
class SyntaxError extends Exception
{
/**
* @var array<string>
*/
protected array $duplicateColumnNames = [];
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToHeaderNotFound(int $offset): self
{
return new self('The header record does not exist or is empty at offset: `'.$offset.'`');
}
public static function dueToInvalidHeaderColumnNames(): self
{
return new self('The header record contains non string colum names.');
}
public static function dueToDuplicateHeaderColumnNames(array $header): self
{
$instance = new self('The header record contains duplicate column names.');
$instance->duplicateColumnNames = array_keys(array_filter(array_count_values($header), fn (int $value): bool => $value > 1));
return $instance;
}
public function duplicateColumnNames(): array
{
return $this->duplicateColumnNames;
}
}

66
vendor/league/csv/src/TabularData.php vendored Normal file
View File

@ -0,0 +1,66 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Iterator;
/**
* @method array nth(int $nth) returns the nth record from the tabular data.
* @method object|null nthAsObject(int $nth, string $className, array $header = []) returns the nth record from the tabular data as an instance of the defined class name.
* @method Iterator map(callable $callback) Run a map over each container record.
* @method Iterator getRecordsAsObject(string $className, array $header = []) Returns the tabular data records as an iterator object containing instance of the defined class name.
*/
interface TabularData
{
/**
* Returns the header associated with the tabular data.
*
* The header must contain unique string or be an empty array
* if no header is specified.
*
* @return array<string>
*/
public function getHeader(): array;
/**
* Returns the tabular data records as an iterator object.
*
* Each record is represented as a simple array containing strings or null values.
*
* If the tabular data has a header record then each record is combined
* to the header record and the header record is removed from the iterator.
*
* If the tabular data is inconsistent. Missing record fields are
* filled with null values while extra record fields are strip from
* the returned object.
*
* @param array<int, string> $header an optional header mapper to use instead of the tabular data header
*
* @return Iterator<array-key, array<array-key, mixed>>
*/
public function getRecords(array $header = []): Iterator;
/**
* Returns a single column from the next record of the tabular data.
*
* By default, if no value is supplied the first column is fetched
*
* @param string|int $index CSV column index
*
* @throws UnableToProcessCsv if the column index is invalid or not found
*
* @return Iterator<int, mixed>
*/
public function fetchColumn(string|int $index = 0): Iterator;
}

View File

@ -0,0 +1,101 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
/**
* Represents a Tabular data.
*
* @template TValue of array
* @template-extends IteratorAggregate<array-key, TValue>
*
* @method Iterator fetchColumnByName(string $name) returns a column from its name
* @method Iterator fetchColumnByOffset(int $offset) returns a column from its offset
* @method array first() returns the first record from the tabular data.
* @method object|null firstAsObject(string $className, array $header = []) returns the first record from the tabular data as an instance of the defined class name.
* @method mixed value(int|string $column = 0) returns a given value from the first element of the tabular data.
* @method bool each(Closure $callback) iterates over each record and passes it to a closure. Iteration is interrupted if the closure returns false
* @method bool exists(Closure $callback) tells whether at least one record satisfies the predicate.
* @method mixed reduce(Closure $callback, mixed $initial = null) reduces the collection to a single value, passing the result of each iteration into the subsequent iteration
* @method Iterator getObjects(string $className, array $header = []) Returns the tabular data records as an iterator object containing instance of the defined class name.
* @method TabularDataReader filter(Query\Predicate|Closure $predicate) returns all the elements of this collection for which your callback function returns `true`
* @method TabularDataReader slice(int $offset, ?int $length = null) extracts a slice of $length elements starting at position $offset from the Collection.
* @method TabularDataReader sorted(Query\Sort|Closure $orderBy) sorts the Collection according to the closure provided see Statement::orderBy method
* @method TabularDataReader select(string|int ...$columnOffsetOrName) extract a selection of the tabular data records columns.
* @method TabularDataReader selectAllExcept(string|int ...$columnOffsetOrName) specifies the names or index of one or more columns to exclude from the selection of the tabular data records columns.
* @method TabularDataReader matchingFirstOrFail(string $expression) extract the first found fragment identifier of the tabular data or fail
* @method TabularDataReader|null matchingFirst(string $expression) extract the first found fragment identifier of the tabular data or return null if none is found
* @method iterable<int, TabularDataReader> matching(string $expression) extract all found fragment identifiers for the tabular data
* @method iterable<TabularDataReader> chunkBy(int $recordsCount) Chunk the TabulaDataReader into smaller TabularDataReader instances of the given size or less.
* @method TabularDataReader mapHeader(array $headers) Returns a new TabulaDataReader with a new set of headers.
*/
interface TabularDataReader extends TabularData, IteratorAggregate, Countable
{
/**
* Returns the tabular data rows as an iterator object containing flat array.
*
* Each row is represented as a simple array containing values.
*
* If the tabular data has a header included as a separate row then each record
* is combined to the header record and the header record is removed from the iteration.
*
* If the tabular data is inconsistent. Missing fields are filled with null values
* while extra record fields are strip from the returned array.
*
* @return Iterator<array-key, TValue>
*/
public function getIterator(): Iterator;
/**
* Returns the number of records contained in the tabular data structure
* excluding the header record.
*/
public function count(): int;
/**
* Returns the next key-value pairs from the tabular data (first
* column is the key, second column is the value).
*
* By default, if no column index is provided:
* - the first column is used to provide the keys
* - the second column is used to provide the value
*
* @param string|int $offset_index The column index to serve as offset
* @param string|int $value_index The column index to serve as value
*
* @throws UnableToProcessCsv if the column index is invalid or not found
*/
public function fetchPairs(string|int $offset_index = 0, string|int $value_index = 1): Iterator;
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.9.0
*
* Returns the nth record from the tabular data.
*
* By default, if no index is provided the first record of the tabular data is returned
*
* @param int $nth_record the tabular data record offset
*
* @throws UnableToProcessCsv if argument is less than 0
*/
#[Deprecated(message:'use League\Csv\TabularDataReader::nth() instead', since:'league/csv:9.9.0')]
public function fetchOne(int $nth_record = 0): array;
}

View File

@ -0,0 +1,47 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Stringable;
/**
* A class to insert records into a CSV Document.
*/
interface TabularDataWriter
{
/**
* Adds multiple records to the CSV document.
*
* @see TabularDataWriter::insertOne
*
* @param iterable<array<null|int|float|string|Stringable>> $records
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertAll(iterable $records): int;
/**
* Adds a single record to a CSV document.
*
* A record is an array that can contain scalar type values, NULL values
* or objects implementing the __toString method.
*
* @param array<null|int|float|string|Stringable> $record
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertOne(array $record): int;
}

View File

@ -0,0 +1,20 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Throwable;
interface UnableToProcessCsv extends Throwable
{
}

View File

@ -0,0 +1,43 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
use Throwable;
/**
* StreamFilterSupportMissing Exception.
*/
class UnavailableFeature extends Exception
{
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToUnsupportedStreamFilterApi(string $className): self
{
return new self('The stream filter API can not be used with a '.$className.' instance.');
}
public static function dueToMissingStreamSeekability(): self
{
return new self('stream does not support seeking.');
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
final class UnavailableStream extends Exception
{
private function __construct(string $message)
{
parent::__construct($message);
}
public static function dueToPathNotFound(string $path): self
{
return new self('`'.$path.'`: failed to open stream: No such file or directory.');
}
public static function dueToForbiddenCloning(string $class_name): self
{
return new self('An object of class '.$class_name.' cannot be cloned.');
}
}

346
vendor/league/csv/src/Writer.php vendored Normal file
View File

@ -0,0 +1,346 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use Deprecated;
use function array_map;
use function array_reduce;
use function implode;
use function restore_error_handler;
use function set_error_handler;
use function str_replace;
use const STREAM_FILTER_WRITE;
/**
* A class to insert records into a CSV Document.
*/
class Writer extends AbstractCsv implements TabularDataWriter
{
protected const ENCLOSE_ALL = 1;
protected const ENCLOSE_NECESSARY = 0;
protected const ENCLOSE_NONE = -1;
protected const STREAM_FILTER_MODE = STREAM_FILTER_WRITE;
/** @var array<Closure(array): bool> callable collection to validate the record before insertion. */
protected array $validators = [];
protected string $newline = "\n";
protected int $flush_counter = 0;
protected ?int $flush_threshold = null;
protected int $enclose_all = self::ENCLOSE_NECESSARY;
/** @var array{0:array<string>,1:array<string>} */
protected array $enclosure_replace = [[], []];
/** @var Closure(array): (int|false) */
protected Closure $insertRecord;
protected function resetProperties(): void
{
parent::resetProperties();
$this->enclosure_replace = [
[$this->enclosure, $this->escape.$this->enclosure.$this->enclosure],
[$this->enclosure.$this->enclosure, $this->escape.$this->enclosure],
];
$this->insertRecord = match ($this->enclose_all) {
self::ENCLOSE_ALL => fn (array $record): int|false => $this->document->fwrite(implode(
$this->delimiter,
array_map(
fn ($content) => $this->enclosure.$content.$this->enclosure,
str_replace($this->enclosure_replace[0], $this->enclosure_replace[1], $record)
)
).$this->newline),
self::ENCLOSE_NONE => fn (array $record): int|false => $this->document->fwrite(implode($this->delimiter, $record).$this->newline),
default => fn (array $record): int|false => $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline),
};
}
/**
* Returns the current end of line sequence characters.
*/
public function getEndOfLine(): string
{
return $this->newline;
}
/**
* Returns the flush threshold.
*/
public function getFlushThreshold(): ?int
{
return $this->flush_threshold;
}
/**
* Tells whether new entries will all be enclosed on writing.
*/
public function encloseAll(): bool
{
return self::ENCLOSE_ALL === $this->enclose_all;
}
/**
* Tells whether new entries will be selectively enclosed on writing
* if the field content requires encoding.
*/
public function encloseNecessary(): bool
{
return self::ENCLOSE_NECESSARY === $this->enclose_all;
}
/**
* Tells whether new entries will never be enclosed on writing.
*/
public function encloseNone(): bool
{
return self::ENCLOSE_NONE === $this->enclose_all;
}
/**
* Adds multiple records to the CSV document.
* @see Writer::insertOne
*
* @throws CannotInsertRecord
* @throws Exception
*/
public function insertAll(iterable $records): int
{
$bytes = 0;
foreach ($records as $record) {
$bytes += $this->insertOne($record);
}
$this->flush_counter = 0;
$this->document->fflush();
return $bytes;
}
/**
* Adds a single record to a CSV document.
*
* A record is an array that can contain scalar type values, NULL values
* or objects implementing the __toString method.
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertOne(array $record): int
{
$record = array_reduce($this->formatters, fn (array $record, callable $formatter): array => $formatter($record), $record);
$this->validateRecord($record);
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$bytes = ($this->insertRecord)($record);
restore_error_handler();
if (false === $bytes) {
throw CannotInsertRecord::triggerOnInsertion($record);
}
if (null === $this->flush_threshold) {
return $bytes;
}
++$this->flush_counter;
if (0 === $this->flush_counter % $this->flush_threshold) {
$this->flush_counter = 0;
$this->document->fflush();
}
return $bytes;
}
/**
* Validates a record.
*
* @throws CannotInsertRecord If the validation failed
*/
protected function validateRecord(array $record): void
{
foreach ($this->validators as $name => $validator) {
true === $validator($record) || throw CannotInsertRecord::triggerOnValidation($name, $record);
}
}
/**
* Adds a record validator.
*
* @param callable(array): bool $validator
*/
public function addValidator(callable $validator, string $validator_name): self
{
$this->validators[$validator_name] = !$validator instanceof Closure ? $validator(...) : $validator;
return $this;
}
/**
* Sets the end of line sequence.
*/
public function setEndOfLine(string $endOfLine): self
{
$this->newline = $endOfLine;
return $this;
}
/**
* Sets the flush threshold.
*
* @throws InvalidArgument if the threshold is an integer less than 1
*/
public function setFlushThreshold(?int $threshold): self
{
if ($threshold === $this->flush_threshold) {
return $this;
}
null === $threshold || 1 <= $threshold || throw InvalidArgument::dueToInvalidThreshold($threshold, __METHOD__);
$this->flush_threshold = $threshold;
$this->flush_counter = 0;
$this->document->fflush();
return $this;
}
public function necessaryEnclosure(): self
{
$this->enclose_all = self::ENCLOSE_NECESSARY;
$this->resetProperties();
return $this;
}
public function forceEnclosure(): self
{
$this->enclose_all = self::ENCLOSE_ALL;
$this->resetProperties();
return $this;
}
public function noEnclosure(): self
{
$this->enclose_all = self::ENCLOSE_NONE;
$this->resetProperties();
return $this;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.8.0
* @codeCoverageIgnore
*
* Format a record.
*
* The returned array must contain
* - scalar types values,
* - NULL values,
* - or objects implementing the __toString() method.
*/
#[Deprecated(message:'no longer affecting the class behaviour', since:'league/csv:9.8.0')]
protected function formatRecord(array $record, callable $formatter): array
{
return $formatter($record);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 9.9.0
* @codeCoverageIgnore
*
* Adds a single record to a CSV Document using PHP algorithm.
*
* @see https://php.net/manual/en/function.fputcsv.php
*/
#[Deprecated(message:'no longer affecting the class behaviour', since:'league/csv:9.9.0')]
protected function addRecord(array $record): int|false
{
return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 9.9.0
* @codeCoverageIgnore
*
* Applies post insertion actions.
*/
#[Deprecated(message:'no longer affecting the class behaviour', since:'league/csv:9.9.0')]
protected function consolidate(): int
{
if (null === $this->flush_threshold) {
return 0;
}
++$this->flush_counter;
if (0 === $this->flush_counter % $this->flush_threshold) {
$this->flush_counter = 0;
$this->document->fflush();
}
return 0;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Writer::getEndOfLine()
* @deprecated Since version 9.10.0
* @codeCoverageIgnore
*
* Returns the current newline sequence characters.
*/
#[Deprecated(message:'use League\Csv\Writer::getEndOfLine()', since:'league/csv:9.10.0')]
public function getNewline(): string
{
return $this->getEndOfLine();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Writer::setEndOfLine()
* @deprecated Since version 9.10.0
* @codeCoverageIgnore
*
* Sets the newline sequence.
*/
#[Deprecated(message:'use League\Csv\Writer::setEndOfLine()', since:'league/csv:9.10.0')]
public function setNewline(string $newline): self
{
return $this->setEndOfLine($newline);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Writer::necessaryEnclosure()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*
* Sets the enclosure threshold to only enclose necessary fields.
*/
#[Deprecated(message:'use League\Csv\Writer::necessaryEnclosure()', since:'league/csv:9.22.0')]
public function relaxEnclosure(): self
{
return $this->necessaryEnclosure();
}
}

307
vendor/league/csv/src/XMLConverter.php vendored Normal file
View File

@ -0,0 +1,307 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Closure;
use Deprecated;
use Dom\Element;
use Dom\XMLDocument;
use DOMDocument;
use DOMElement;
use DOMException;
use Exception;
use RuntimeException;
use Throwable;
use ValueError;
use function class_exists;
use function extension_loaded;
use function in_array;
use function is_bool;
use function strtolower;
use function strtoupper;
/**
* Converts tabular data into a DOMDocument object.
*/
class XMLConverter
{
/** XML Root name. */
protected string $root_name = 'csv';
/** XML Node name. */
protected string $record_name = 'row';
/** XML Item name. */
protected ?string $field_name = 'cell';
/** XML column attribute name. */
protected string $column_attr = '';
/** XML offset attribute name. */
protected string $offset_attr = '';
/** @var ?Closure(array, array-key): array */
protected ?Closure $formatter = null;
/**
*
* @throws RuntimeException If the extension is not present
* @throws ValueError If the XML class used is invalid
*/
private static function newXmlDocument(string $xml_class): DOMDocument|XMLDocument
{
return match (true) {
!extension_loaded('dom') => throw new RuntimeException('The DOM extension is not loaded.'),
!in_array($xml_class, [XMLDocument::class , DOMDocument::class], true) => throw new ValueError('The xml class is invalid.'),
XMLDocument::class === $xml_class && class_exists(XMLDocument::class) => XMLDocument::createEmpty(),
default => new DOMDocument(encoding: 'UTF-8'),
};
}
public static function supportsHeader(array $header): bool
{
$document = self::newXmlDocument(XMLDocument::class);
foreach ($header as $header_value) {
try {
$document->createElement($header_value);
} catch (Throwable) {
return false;
}
}
return [] !== $header;
}
public function __construct()
{
}
/**
* XML root element setter.
*
* @throws DOMException
*/
public function rootElement(string $node_name): self
{
$clone = clone $this;
$clone->root_name = (string) $this->filterElementName($node_name);
return $clone;
}
/**
* XML Record element setter.
*
* @throws DOMException
*/
public function recordElement(string $node_name, string $record_offset_attribute_name = ''): self
{
$clone = clone $this;
$clone->record_name = (string) $this->filterElementName($node_name);
$clone->offset_attr = $this->filterAttributeName($record_offset_attribute_name);
return $clone;
}
/**
* XML Field element setter.
*
* @throws DOMException
*/
public function fieldElement(?string $node_name, string $fieldname_attribute_name = ''): self
{
$clone = clone $this;
$clone->field_name = $this->filterElementName($node_name);
$clone->column_attr = $this->filterAttributeName($fieldname_attribute_name);
return $clone;
}
/**
* Set a callback to format each item before json encode.
*
* @param ?callable(array, array-key): array $formatter
*/
public function formatter(?callable $formatter): self
{
$clone = clone $this;
$clone->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);
return $clone;
}
/**
* Sends and makes the XML structure downloadable via HTTP.
*.
* Returns the number of characters read from the handle and passed through to the output.
*
* @throws Exception
*/
public function download(iterable $records, ?string $filename = null, string $encoding = 'utf-8', bool $formatOutput = false): int|false
{
/** @var XMLDocument|DOMDocument $document */
$document = self::newXmlDocument(XMLDocument::class);
$document->appendChild($this->import($records, $document));
if (null !== $filename) {
HttpHeaders::forFileDownload($filename, 'application/xml; charset='.strtolower($encoding));
}
$document->formatOutput = $formatOutput;
if ($document instanceof DOMDocument) {
$document->encoding = strtoupper($encoding);
return $document->save('php://output');
}
return $document->saveXmlFile('php://output');
}
/**
* Creates a new DOMElement related to the given DOMDocument.
*
* **DOES NOT** attach to the DOMDocument
*/
public function import(iterable $records, DOMDocument|XMLDocument $doc): DOMElement|Element
{
if (null !== $this->formatter) {
$records = MapIterator::fromIterable($records, $this->formatter);
}
$root = $doc->createElement($this->root_name);
foreach ($records as $offset => $record) {
$root->appendChild($this->recordToElement($doc, $record, $offset));
}
return $root;
}
/**
* Converts a CSV record into a DOMElement and
* adds its offset as DOMElement attribute.
*/
protected function recordToElement(DOMDocument|XMLDocument $document, array $record, int $offset): DOMElement|Element
{
$node = $document->createElement($this->record_name);
foreach ($record as $node_name => $value) {
$node->appendChild($this->fieldToElement($document, (string) $value, $node_name));
}
if ('' !== $this->offset_attr) {
$node->setAttribute($this->offset_attr, (string) $offset);
}
return $node;
}
/**
* Converts Cell to Item.
*
* Converts the CSV item into a DOMElement and adds the item offset
* as attribute to the returned DOMElement
*/
protected function fieldToElement(DOMDocument|XMLDocument $document, string $value, int|string $node_name): DOMElement|Element
{
$node_name = (string) $node_name;
$item = $document->createElement($this->field_name ?? $node_name);
$item->appendChild($document->createTextNode($value));
if ('' !== $this->column_attr) {
$item->setAttribute($this->column_attr, $node_name);
}
return $item;
}
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* Filters XML element name.
*
* @throws DOMException If the Element name is invalid
*/
protected function filterElementName(?string $value): ?string
{
if (null === $value) {
return null;
}
return self::newXmlDocument(XMLDocument::class)->createElement($value)->tagName;
}
/**
* Filters XML attribute name.
*
* @param string $value Element name
*
* @throws DOMException If the Element attribute name is invalid
*/
protected function filterAttributeName(string $value): string
{
if ('' === $value) {
return $value;
}
$element = self::newXmlDocument(XMLDocument::class)->createElement('foo');
$element->setAttribute($value, 'foo');
return $value;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see XMLConverter::import()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*
* Converts a Record collection into a DOMDocument.
*/
#[Deprecated(message:'use League\Csv\XMLConverter::impoprt()', since:'league/csv:9.22.0')]
public function convert(iterable $records): DOMDocument
{
$document = new DOMDocument(encoding: 'UTF-8');
$document->appendChild($this->import($records, $document));
return $document;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see XMLConverter::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*
* Returns an new instance.
*/
#[Deprecated(message:'use League\Csv\XMLConverter::__construct()', since:'league/csv:9.22.0')]
public static function create(): self
{
return new self();
}
}

57
vendor/league/csv/src/functions.php vendored Normal file
View File

@ -0,0 +1,57 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Deprecated;
/**
* DEPRECATION WARNING! This namespace function will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see Bom::tryFromSequence()
* @codeCoverageIgnore
*
* Returns the BOM sequence found at the start of the string.
*
* If no valid BOM sequence is found an empty string is returned
*/
#[Deprecated(message:'use League\Csv\Bom::tryFromSequence()', since:'league/csv:9.7.0')]
function bom_match(string $str): string
{
return Bom::tryFromSequence($str)?->value ?? '';
}
/**
* DEPRECATION WARNING! This namespace function will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see Info::getDelimiterStats()
* @codeCoverageIgnore
*
* Detect Delimiters usage in a {@link Reader} object.
*
* Returns a associative array where each key represents
* a submitted delimiter and each value the number of CSV fields found
* when processing at most $limit CSV records with the given delimiter
*
* @param array<string> $delimiters
* @param int<-1, max> $limit
*
* @return array<string,int>
*/
#[Deprecated(message:'use League\Csv\Info::getDelimiterStats() instead', since:'league/csv:9.8.0')]
function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array
{
return Info::getDelimiterStats($csv, $delimiters, $limit);
}

View File

@ -0,0 +1,14 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('League\Csv\delimiter_detect')) {
require __DIR__.'/functions.php';
}