check
This commit is contained in:
parent
b15a1f6a46
commit
eb2ac3c16d
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -61,4 +62,4 @@
|
|||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/Users/sutanarsyahnugraha/Documents/Joki/analisis/storage/app/public
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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.'".');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.'`');
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
Loading…
Reference in New Issue