Laravel
This commit is contained in:
parent
793014673e
commit
910e3e6c12
|
@ -0,0 +1,58 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:D9aDaLt//OYmd8K1IvEfOwqWwAUEdQXR078ZNNloGDU=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=absensikukar
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
|
@ -8,6 +8,7 @@ use App\Models\Location;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AttendanceController extends Controller
|
||||
{
|
||||
|
@ -27,12 +28,18 @@ class AttendanceController extends Controller
|
|||
});
|
||||
}
|
||||
|
||||
if ($request->has('start_date') && !empty($request->start_date)) {
|
||||
$data->where('start_date', '>=', $request->start_date);
|
||||
}
|
||||
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||
try {
|
||||
$startDate = Carbon::parse($request->start_date)->startOfDay();
|
||||
$endDate = Carbon::parse($request->end_date)->endOfDay();
|
||||
|
||||
if ($request->has('end_date') && !empty($request->end_date)) {
|
||||
$data->where('end_date', '<=', $request->end_date);
|
||||
$data->whereBetween('date', [$startDate, $endDate]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Format tanggal tidak valid.',
|
||||
'error' => $e->getMessage()
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('status') && !empty($request->status)) {
|
||||
|
@ -83,37 +90,36 @@ class AttendanceController extends Controller
|
|||
public function approve(Request $request, string $id)
|
||||
{
|
||||
$attendance = Attendance::find($id);
|
||||
|
||||
if (!$attendance) {
|
||||
return response()->json([
|
||||
'message' => 'Data tidak ditemukan.',
|
||||
'data' => null
|
||||
], 422);
|
||||
return response()->json(['message'=>'Data tidak ditemukan.'], 422);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'status' => 'required|in:accepted,rejected',
|
||||
]);
|
||||
// parse check-in & check-out
|
||||
$in = \Carbon\Carbon::parse("{$attendance->date} {$attendance->time}");
|
||||
$out = \Carbon\Carbon::parse("{$attendance->checkout_date} {$attendance->checkout_time}");
|
||||
$durationMinutes = $out->diffInMinutes($in);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => Str::ucfirst($validator->errors()->first()),
|
||||
'data' => null
|
||||
], 422);
|
||||
// jam pulang 17:30 di hari yang sama
|
||||
$endOfWork = $in->copy()->setTime(17,00,0);
|
||||
|
||||
// tentukan status baru
|
||||
if ($durationMinutes < 4*60) {
|
||||
$newStatus = 'jam_kerja_kurang';
|
||||
} elseif ($out->gt($endOfWork)) {
|
||||
$newStatus = 'lembur';
|
||||
} else {
|
||||
$newStatus = 'accepted';
|
||||
}
|
||||
|
||||
$attendance->update([
|
||||
'status' => $request->status,
|
||||
]);
|
||||
|
||||
$message = $request->status === 'accepted' ? 'Absensi berhasil disetujui.' : 'Absensi berhasil ditolak.';
|
||||
$attendance->update(['status'=>$newStatus]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $message,
|
||||
'message' => "Absensi diproses: status “{$newStatus}”.",
|
||||
'data' => $attendance->load(['user.profile','location'])
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
public function statistics(Request $request)
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
@ -146,4 +152,18 @@ class AttendanceController extends Controller
|
|||
'radius' => $area->radius,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$attendance = Attendance::find($id);
|
||||
|
||||
if (!$attendance) {
|
||||
return response()->json(['message' => 'Data tidak ditemukan.'], 404);
|
||||
}
|
||||
|
||||
$attendance->delete();
|
||||
|
||||
return response()->json(['message' => 'Data absensi berhasil dihapus.'], 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api\Admin;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Permission;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
@ -75,10 +77,28 @@ class PermissionController extends Controller
|
|||
], 422);
|
||||
}
|
||||
|
||||
// return response()->json([
|
||||
// 'message' => 'Data berhasil diambil.',
|
||||
// 'data' => $data
|
||||
// ], 200);
|
||||
return response()->json([
|
||||
'message' => 'Data berhasil diambil.',
|
||||
'data' => $data
|
||||
'data' => [
|
||||
'id' => $data->id,
|
||||
'user' => $data->user,
|
||||
'category' => $data->category,
|
||||
'start_date' => $data->start_date,
|
||||
'end_date' => $data->end_date,
|
||||
'reason' => $data->reason,
|
||||
'status' => $data->status,
|
||||
'created_at' => $data->created_at,
|
||||
'approver' => $data->approver,
|
||||
'proof_photo' => $data->proof_photo, // nama file saja
|
||||
'proof_photo_url' => $data->proof_photo_url, // URL lengkap ke file foto
|
||||
'duration' => $data->duration
|
||||
]
|
||||
], 200);
|
||||
|
||||
}
|
||||
|
||||
public function approve(Request $request, string $id)
|
||||
|
@ -139,4 +159,27 @@ class PermissionController extends Controller
|
|||
'data' => $stats
|
||||
], 200);
|
||||
}
|
||||
|
||||
// Di Admin/PermissionController.php
|
||||
|
||||
public function ranking(Request $request)
|
||||
{
|
||||
$sort = $request->input('sort', 'desc');
|
||||
$currentMonth = Carbon::now()->month;
|
||||
$currentYear = Carbon::now()->year;
|
||||
|
||||
$ranking = User::select('users.id', 'users.name')
|
||||
->leftJoin('permissions', function($join) use ($currentMonth, $currentYear) {
|
||||
$join->on('users.id', '=', 'permissions.user_id')
|
||||
->whereMonth('permissions.start_date', $currentMonth)
|
||||
->whereYear('permissions.start_date', $currentYear)
|
||||
->where('permissions.status', 'accepted');
|
||||
})
|
||||
->selectRaw('COUNT(permissions.id) as permission_count')
|
||||
->groupBy('users.id', 'users.name')
|
||||
->orderBy('permission_count', $sort)
|
||||
->paginate(10);
|
||||
|
||||
return view('ranking.index', compact('ranking', 'sort'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\User;
|
||||
use App\Models\Permission;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ToprankController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$month = $request->input('month', Carbon::now()->month);
|
||||
$year = $request->input('year', Carbon::now()->year);
|
||||
$sort = $request->input('sort', 'desc');
|
||||
|
||||
$ranking = User::select('users.id', 'users.name')
|
||||
->where('role', 'karyawan')
|
||||
->withCount(['permissions' => function($query) use ($month, $year) {
|
||||
$query->whereMonth('start_date', $month)
|
||||
->whereYear('start_date', $year);
|
||||
}])
|
||||
->orderBy('permissions_count', $sort)
|
||||
->paginate(10);
|
||||
|
||||
$months = [
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||
];
|
||||
|
||||
return view('dashboard.toprank', [
|
||||
'ranking' => $ranking,
|
||||
'months' => $months,
|
||||
'currentMonth' => $month,
|
||||
'currentYear' => $year,
|
||||
'sort' => $sort
|
||||
]);
|
||||
}
|
||||
|
||||
public function userPermissionsWeb($userId, Request $request)
|
||||
{
|
||||
try {
|
||||
$month = $request->input('month', Carbon::now()->month);
|
||||
$year = $request->input('year', Carbon::now()->year);
|
||||
|
||||
$user = User::where('id', $userId)
|
||||
->where('role', 'karyawan')
|
||||
->firstOrFail();
|
||||
|
||||
$permissions = Permission::where('user_id', $userId)
|
||||
->whereMonth('start_date', $month)
|
||||
->whereYear('start_date', $year)
|
||||
->orderBy('start_date', 'desc')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'category' => $item->category,
|
||||
'reason' => $item->reason,
|
||||
'start_date' => $item->start_date,
|
||||
'end_date' => $item->end_date,
|
||||
'status' => $item->status,
|
||||
'proof_photo' => $item->proof_photo,
|
||||
'created_at' => $item->created_at,
|
||||
'approved_at' => $item->approved_at,
|
||||
'approved_by' => optional($item->approver)->name
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'all' => $permissions,
|
||||
'accepted' => $permissions->where('status', 'accepted')->values(),
|
||||
'pending' => $permissions->where('status', 'pending')->values(),
|
||||
'rejected' => $permissions->where('status', 'rejected')->values()
|
||||
],
|
||||
'stats' => [
|
||||
'accepted' => $permissions->where('status', 'accepted')->count(),
|
||||
'pending' => $permissions->where('status', 'pending')->count(),
|
||||
'rejected' => $permissions->where('status', 'rejected')->count()
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat data izin: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// TOP RANK ABSEN
|
||||
public function absensi(Request $request)
|
||||
{
|
||||
$month = $request->input('month', Carbon::now()->month);
|
||||
$year = $request->input('year', Carbon::now()->year);
|
||||
$sort = $request->input('sort', 'desc');
|
||||
|
||||
$ranking = User::select('users.id', 'users.name')
|
||||
->where('role', 'karyawan')
|
||||
->withCount(['attendances as total_absen' => function ($query) use ($month, $year) {
|
||||
$query->whereMonth('created_at', $month)
|
||||
->whereYear('created_at', $year);
|
||||
}])
|
||||
->orderBy('total_absen', $sort)
|
||||
->paginate(10);
|
||||
|
||||
$months = [
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||
];
|
||||
|
||||
if ($request->ajax()) {
|
||||
// Jika request AJAX, hanya kirim partial table
|
||||
return view('dashboard.toprank_absensi', [
|
||||
'ranking' => $ranking,
|
||||
'months' => $months,
|
||||
'currentMonth' => $month,
|
||||
'currentYear' => $year,
|
||||
'sort' => $sort
|
||||
]);
|
||||
}
|
||||
|
||||
// Jika normal page load
|
||||
return view('dashboard.toprank_absensi', [
|
||||
'ranking' => $ranking,
|
||||
'months' => $months,
|
||||
'currentMonth' => $month,
|
||||
'currentYear' => $year,
|
||||
'sort' => $sort
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function userAttendances($userId, Request $request)
|
||||
{
|
||||
try {
|
||||
$month = $request->input('month', Carbon::now()->month);
|
||||
$year = $request->input('year', Carbon::now()->year);
|
||||
|
||||
$attendances = Attendance::with(['location', 'user'])
|
||||
->where('user_id', $userId)
|
||||
->whereMonth('created_at', $month)
|
||||
->whereYear('created_at', $year)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $attendances
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat data absensi: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -37,6 +37,14 @@ class AuthController extends Controller
|
|||
], 401);
|
||||
}
|
||||
|
||||
// ✅ Jika login dari mobile dan rolenya admin, tolak
|
||||
if ($request->header('X-Platform') === 'mobile' && $user->role === 'admin') {
|
||||
return response()->json([
|
||||
'message' => 'Admin tidak dapat login melalui aplikasi mobile.',
|
||||
'data' => null
|
||||
], 403);
|
||||
}
|
||||
|
||||
$user->tokens()->delete();
|
||||
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
@ -66,6 +74,7 @@ class AuthController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
public function getUserByToken($token)
|
||||
{
|
||||
$accessToken = PersonalAccessToken::findToken($token);
|
||||
|
|
|
@ -46,7 +46,7 @@ class AttendanceController extends Controller
|
|||
], 422);
|
||||
}
|
||||
|
||||
if ($request->type == 'out') {
|
||||
if ($request->type === 'out') {
|
||||
$clockInRecord = Attendance::where('user_id', $user->id)
|
||||
->where('date', $today)
|
||||
->where('type', 'in')
|
||||
|
@ -58,7 +58,20 @@ class AttendanceController extends Controller
|
|||
'data' => null
|
||||
], 422);
|
||||
}
|
||||
|
||||
$checkInTime = Carbon::parse($clockInRecord->time);
|
||||
$now = Carbon::now();
|
||||
$workDurationInMinutes = $checkInTime->diffInMinutes($now);
|
||||
|
||||
if ($workDurationInMinutes < 240) { // kurang dari 4 jam
|
||||
$status = 'jam_kerja_kurang';
|
||||
} elseif ($now->format('H:i') > '17:00') {
|
||||
$status = 'lembur';
|
||||
} else {
|
||||
$status = 'accepted';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$photoPath = null;
|
||||
if ($request->hasFile('photo')) {
|
||||
|
@ -67,14 +80,29 @@ class AttendanceController extends Controller
|
|||
$photoPath = $photo->storeAs('attendances', $filename, 'public');
|
||||
}
|
||||
|
||||
// Tentukan status late atau accepted
|
||||
$now = Carbon::now();
|
||||
$status = 'accepted';
|
||||
if ($request->type == 'in' && $now->format('H:i:s') > '08:00:00') {
|
||||
$status = 'late';
|
||||
|
||||
if ($request->type == 'in') {
|
||||
$status = $now->format('H:i:s') > '08:00:00' ? 'late' : 'accepted';
|
||||
} else if ($request->type == 'out') {
|
||||
$clockIn = Attendance::where('user_id', $user->id)
|
||||
->where('date', $today)
|
||||
->where('type', 'in')
|
||||
->first();
|
||||
|
||||
if ($clockIn) {
|
||||
$clockInTime = Carbon::parse($clockIn->time);
|
||||
$diffInHours = $clockInTime->diffInMinutes($now) / 60;
|
||||
|
||||
if ($diffInHours < 4) {
|
||||
$status = 'jam_kerja_kurang';
|
||||
} else {
|
||||
$status = $now->format('H:i:s') > '17:00:00' ? 'lembur' : 'accepted';
|
||||
}
|
||||
} else {
|
||||
$status = 'jam_kerja_kurang';
|
||||
}
|
||||
if ($request->type == 'out' && $now->format('H:i:s') > '17:00:00') {
|
||||
$status = 'late';
|
||||
}
|
||||
|
||||
$nearestLocation = $this->findNearestLocation($request->latitude, $request->longitude);
|
||||
|
@ -89,7 +117,7 @@ class AttendanceController extends Controller
|
|||
'user_id' => $user->id,
|
||||
'date' => $today,
|
||||
'type' => $request->type,
|
||||
'time' => Carbon::now()->format('H:i:s'),
|
||||
'time' => $now->format('H:i:s'),
|
||||
'photo' => $photoPath,
|
||||
'latitude' => $request->latitude,
|
||||
'longitude' => $request->longitude,
|
||||
|
@ -103,6 +131,8 @@ class AttendanceController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
// method lain tidak diubah
|
||||
|
||||
public function history(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
@ -208,7 +238,6 @@ class AttendanceController extends Controller
|
|||
$location->center_lat,
|
||||
$location->center_lng
|
||||
);
|
||||
// $distance dalam kilometer, radius dalam meter
|
||||
if ($distance * 1000 <= $location->radius) {
|
||||
return $location;
|
||||
}
|
||||
|
@ -218,7 +247,7 @@ class AttendanceController extends Controller
|
|||
|
||||
private function calculateDistance($lat1, $lon1, $lat2, $lon2)
|
||||
{
|
||||
$earthRadius = 6371; // km
|
||||
$earthRadius = 6371;
|
||||
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLon = deg2rad($lon2 - $lon1);
|
||||
|
@ -231,7 +260,6 @@ class AttendanceController extends Controller
|
|||
|
||||
public function areas()
|
||||
{
|
||||
// Ambil semua Location. Jika perlu filter aktif, tambahkan where.
|
||||
$locations = Location::all()->map(function($loc) {
|
||||
return [
|
||||
'id' => $loc->id,
|
||||
|
|
|
@ -25,7 +25,8 @@ class Permission extends Model
|
|||
'approved_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $appends = ['duration'];
|
||||
// protected $appends = ['duration'];
|
||||
protected $appends = ['duration', 'proof_photo_url'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ return new class extends Migration {
|
|||
$table->decimal('latitude', 10, 8);
|
||||
$table->decimal('longitude', 11, 8);
|
||||
$table->foreignId('location_id')->nullable()->constrained('locations');
|
||||
$table->enum('status', ['pending', 'accepted', 'rejected'])->default('pending');
|
||||
$table->enum('status', ['pending', 'accepted', 'rejected', 'jam_kerja_kurang', 'lembur', 'late'])->default('pending');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -220,6 +220,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label><strong>Foto Absensi:</strong></label>
|
||||
<div id="detailPhotoWrapper">
|
||||
<span class="text-muted">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><strong>Catatan:</strong></label>
|
||||
<p id="detailNotes">-</p>
|
||||
|
@ -400,6 +408,13 @@ function initDataTable() {
|
|||
`;
|
||||
}
|
||||
|
||||
// Tambahkan tombol hapus di sini
|
||||
buttons += `
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteAttendance(${data})" title="Hapus">
|
||||
<i class="ti ti-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
return buttons;
|
||||
}
|
||||
}
|
||||
|
@ -473,6 +488,17 @@ function viewAttendance(id) {
|
|||
$('#detailLocation').text(attendance.location ? attendance.location.name : '-');
|
||||
$('#detailCoordinates').text(attendance.latitude && attendance.longitude ?
|
||||
`${attendance.latitude}, ${attendance.longitude}` : '-');
|
||||
if (attendance.photo) {
|
||||
const photoUrl = `/storage/${attendance.photo}`;
|
||||
$('#detailPhotoWrapper').html(`
|
||||
<a href="${photoUrl}" target="_blank">
|
||||
<img src="${photoUrl}" alt="Foto Absensi" style="max-width: 100%; max-height: 300px; border-radius: 8px;">
|
||||
</a>
|
||||
`);
|
||||
} else {
|
||||
$('#detailPhotoWrapper').html(`<span class="text-muted">Tidak ada foto</span>`);
|
||||
}
|
||||
|
||||
$('#detailNotes').text(attendance.notes || '-');
|
||||
$('#detailCreatedAt').text(new Date(attendance.created_at).toLocaleDateString('id-ID'));
|
||||
$('#detailUpdatedAt').text(new Date(attendance.updated_at).toLocaleDateString('id-ID'));
|
||||
|
@ -526,6 +552,47 @@ function approveAttendance(id) {
|
|||
});
|
||||
}
|
||||
|
||||
function deleteAttendance(id) {
|
||||
Swal.fire({
|
||||
title: 'Yakin ingin menghapus?',
|
||||
text: 'Data absensi yang dihapus tidak dapat dikembalikan!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Ya, hapus!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: `/api/admin/attendances/${id}`,
|
||||
type: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': getAuthorizationHeader(),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
success: function(response) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: response.message
|
||||
});
|
||||
attendanceTable.ajax.reload();
|
||||
loadStatistics();
|
||||
},
|
||||
error: function(xhr) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal!',
|
||||
text: xhr.responseJSON?.message || 'Terjadi kesalahan.'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('#approveAttendanceForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<footer class="footer">
|
||||
<div class="d-sm-flex justify-content-center justify-content-sm-between">
|
||||
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2025.
|
||||
<a href="/" target="_blank">Absensiku.</span>
|
||||
<a target="_blank">Absensiku.</span>
|
||||
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made
|
||||
with <i class="ti-heart text-danger ml-1"></i></span>
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,29 @@
|
|||
<i class="ti-menu menu-icon"></i>
|
||||
<span class="menu-title">Attendances</span>
|
||||
</a>
|
||||
</li>
|
||||
{{-- <li class="nav-item {{ Route::is('toprank.index') ? 'active' : '' }}">
|
||||
<a class="nav-link" href="{{ route('toprank.index') }}">
|
||||
<i class="ti-menu menu-icon"></i>
|
||||
<span class="menu-title">TopRank</span>
|
||||
</a>
|
||||
</li> --}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" data-toggle="collapse" href="#toprankMenu" aria-expanded="false" aria-controls="toprankMenu">
|
||||
<i class="menu-icon ti-crown"></i>
|
||||
<span class="menu-title">TopRank</span>
|
||||
<i class="menu-arrow"></i>
|
||||
</a>
|
||||
<div class="collapse" id="toprankMenu">
|
||||
<ul class="nav flex-column sub-menu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('toprank.izin') }}">Izin</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('toprank.absensi') }}">Absensi</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item {{ Route::is('reports.index') ? 'active' : '' }}">
|
||||
<a class="nav-link" href="{{ route('reports.index') }}">
|
||||
|
@ -43,6 +66,7 @@
|
|||
<span class="menu-title">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- partial -->
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
<label><strong>Alasan:</strong></label>
|
||||
<p id="detailReason">-</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><strong>Bukti Foto:</strong></label>
|
||||
<div id="detailPhotoWrapper" class="mt-2"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
|
@ -429,7 +433,16 @@ function viewPermission(id) {
|
|||
$('#detailReason').text(permission.reason);
|
||||
$('#detailCreatedAt').text(new Date(permission.created_at).toLocaleDateString('id-ID'));
|
||||
$('#detailApprover').text(permission.approver ? permission.approver.name : '-');
|
||||
|
||||
if (permission.proof_photo) {
|
||||
const photoUrl = `/storage/${permission.proof_photo}`;
|
||||
$('#detailPhotoWrapper').html(`
|
||||
<a href="${photoUrl}" target="_blank">
|
||||
<img src="${photoUrl}" alt="Bukti Foto" style="max-width: 100%; max-height: 300px; border-radius: 8px; border: 1px solid #ccc;">
|
||||
</a>
|
||||
`);
|
||||
} else {
|
||||
$('#detailPhotoWrapper').html(`<span class="text-muted">Tidak ada foto</span>`);
|
||||
}
|
||||
$('#detailPermissionModal').modal('show');
|
||||
},
|
||||
error: function(xhr) {
|
||||
|
|
|
@ -179,9 +179,9 @@
|
|||
<button type="button" class="btn btn-export" id="exportAttendanceExcel" title="Export Excel">
|
||||
<span class="material-icons align-middle">file_download</span> Export Excel
|
||||
</button>
|
||||
<button type="button" class="btn btn-export" id="exportAttendancePdf" title="Export PDF">
|
||||
{{-- <button type="button" class="btn btn-export" id="exportAttendancePdf" title="Export PDF">
|
||||
<span class="material-icons align-middle">print</span> Export PDF
|
||||
</button>
|
||||
</button> --}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -276,9 +276,9 @@
|
|||
<button type="button" class="btn btn-export" id="exportPermissionExcel" title="Export Excel">
|
||||
<span class="material-icons align-middle">file_download</span> Export Excel
|
||||
</button>
|
||||
<button type="button" class="btn btn-export" id="exportPermissionPdf" title="Export PDF">
|
||||
{{-- <button type="button" class="btn btn-export" id="exportPermissionPdf" title="Export PDF">
|
||||
<span class="material-icons align-middle">print</span> Export PDF
|
||||
</button>
|
||||
</button> --}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -0,0 +1,350 @@
|
|||
@extends('dashboard.base')
|
||||
|
||||
@section('title', 'Ranking Izin Karyawan')
|
||||
|
||||
@section('content')
|
||||
<div class="content-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-md-12 grid-margin">
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
|
||||
<h3 class="font-weight-bold">Ranking Karyawan Berdasarkan Izin</h3>
|
||||
<h6 class="font-weight-normal mb-0" id="monthYearDisplay">
|
||||
Bulan: {{ $months[$currentMonth] }} {{ $currentYear }}
|
||||
<span class="text"> | Urutan: {{ $sort == 'desc' ? 'Terbanyak' : 'Tersedikit' }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-12 col-xl-4 text-right">
|
||||
<div class="dropdown d-inline mr-2">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="monthDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Pilih Bulan
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="monthDropdown">
|
||||
@foreach($months as $key => $month)
|
||||
<a class="dropdown-item month-select" href="#" data-month="{{ $key }}">{{ $month }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-inline">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="sortDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ $sort == 'desc' ? 'Terbanyak Izin' : 'Tersedikit Izin' }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="sortDropdown">
|
||||
<a class="dropdown-item sort-select" href="#" data-sort="desc">Terbanyak Izin</a>
|
||||
<a class="dropdown-item sort-select" href="#" data-sort="asc">Tersedikit Izin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ... (bagian tabel tetap sama) ... -->
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 grid-margin stretch-card">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Nama Karyawan</th>
|
||||
<th>Jumlah Izin</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rankingTableBody">
|
||||
@forelse($ranking as $index => $user)
|
||||
<tr>
|
||||
<td>{{ $ranking->firstItem() + $index }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->permissions_count }}</td>
|
||||
<td>
|
||||
<a href="#" class="btn btn-sm btn-info view-detail"
|
||||
data-user-id="{{ $user->id }}"
|
||||
data-user-name="{{ $user->name }}">
|
||||
<i class="ti-eye"></i> Lihat
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">Tidak ada data izin bulan ini</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
{{ $ranking->appends([
|
||||
'month' => $currentMonth,
|
||||
'year' => $currentYear,
|
||||
'sort' => $sort
|
||||
])->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@endsection
|
||||
|
||||
<!-- Modal Detail Izin -->
|
||||
<div class="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="detailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title" id="detailModalLabel">
|
||||
Detail Izin: <span id="userName"></span>
|
||||
</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Statistik Izin -->
|
||||
<div class="row text-center mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<h4 id="acceptedCount">0</h4>
|
||||
<p>Disetujui</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<h4 id="pendingCount">0</h4>
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body">
|
||||
<h4 id="rejectedCount">0</h4>
|
||||
<p>Ditolak</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Detail -->
|
||||
<ul class="nav nav-tabs" id="permissionTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="all-tab" data-toggle="tab" href="#allTab" role="tab">Semua</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="accepted-tab" data-toggle="tab" href="#acceptedTab" role="tab">Disetujui</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="pending-tab" data-toggle="tab" href="#pendingTab" role="tab">Pending</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="rejected-tab" data-toggle="tab" href="#rejectedTab" role="tab">Ditolak</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-3" id="permissionTabContent">
|
||||
<div class="tab-pane fade show active" id="allTab" role="tabpanel">
|
||||
<div id="allPermissions"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="acceptedTab" role="tabpanel">
|
||||
<div id="acceptedPermissions"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="pendingTab" role="tabpanel">
|
||||
<div id="pendingPermissions"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="rejectedTab" role="tabpanel">
|
||||
<div id="rejectedPermissions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@push('script')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let currentMonth = {{ $currentMonth }};
|
||||
let currentYear = {{ $currentYear }};
|
||||
let currentSort = '{{ $sort }}';
|
||||
|
||||
// Fungsi untuk memuat data ranking
|
||||
function loadRankingData() {
|
||||
$.ajax({
|
||||
url: '/toprank',
|
||||
type: 'GET',
|
||||
data: {
|
||||
month: currentMonth,
|
||||
year: currentYear,
|
||||
sort: currentSort
|
||||
},
|
||||
success: function(response) {
|
||||
$('#rankingTableBody').html($(response).find('#rankingTableBody').html());
|
||||
$('.pagination').html($(response).find('.pagination').html());
|
||||
$('#monthYearDisplay').text($(response).find('#monthYearDisplay').text());
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('Error loading ranking data:', xhr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle pemilihan bulan
|
||||
$(document).on('click', '.month-select', function(e) {
|
||||
e.preventDefault();
|
||||
currentMonth = $(this).data('month');
|
||||
loadRankingData();
|
||||
});
|
||||
|
||||
// Handle pemilihan sorting
|
||||
$(document).on('click', '.sort-select', function(e) {
|
||||
e.preventDefault();
|
||||
currentSort = $(this).data('sort');
|
||||
loadRankingData();
|
||||
});
|
||||
|
||||
|
||||
// Fungsi untuk render permissions
|
||||
function renderPermissions(permissions, targetId) {
|
||||
let html = '<div class="list-group">';
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
permissions.forEach(function(permission) {
|
||||
let statusBadge = '';
|
||||
let statusClass = '';
|
||||
|
||||
if (permission.status === 'accepted') {
|
||||
statusBadge = 'Disetujui';
|
||||
statusClass = 'success';
|
||||
} else if (permission.status === 'pending') {
|
||||
statusBadge = 'Pending';
|
||||
statusClass = 'warning';
|
||||
} else {
|
||||
statusBadge = 'Ditolak';
|
||||
statusClass = 'danger';
|
||||
}
|
||||
|
||||
let proofPhoto = permission.proof_photo ?
|
||||
`<a href="/storage/permissions/${permission.proof_photo}" target="_blank" class="btn btn-sm btn-link mt-1">
|
||||
<i class="ti-image"></i> Lihat Bukti
|
||||
</a>` : '';
|
||||
|
||||
let approvedInfo = permission.approved_at ?
|
||||
`<small class="text-muted d-block mt-1">
|
||||
Disetujui pada: ${new Date(permission.approved_at).toLocaleDateString('id-ID')}
|
||||
</small>` : '';
|
||||
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1 font-weight-bold">${permission.category}</h6>
|
||||
<p class="mb-1">${permission.start_date} s/d ${permission.end_date}</p>
|
||||
<p class="mb-1 small">Alasan: ${permission.reason}</p>
|
||||
</div>
|
||||
<span class="badge badge-${statusClass}">${statusBadge}</span>
|
||||
</div>
|
||||
${proofPhoto}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Diajukan pada: ${new Date(permission.created_at).toLocaleDateString('id-ID')}
|
||||
</small>
|
||||
${approvedInfo}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += `
|
||||
<div class="list-group-item text-center py-4">
|
||||
<i class="ti-info-alt" style="font-size: 2rem;"></i>
|
||||
<p class="mt-2 mb-0">Tidak ada data izin</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('#' + targetId).html(html);
|
||||
}
|
||||
|
||||
// Handle modal detail
|
||||
$(document).on('click', '.view-detail', function(e) {
|
||||
e.preventDefault();
|
||||
const userId = $(this).data('user-id');
|
||||
const userName = $(this).data('user-name');
|
||||
|
||||
// Tampilkan modal
|
||||
const modal = $('#detailModal');
|
||||
modal.find('#userName').text(userName);
|
||||
modal.modal('show');
|
||||
|
||||
// Reset konten modal
|
||||
modal.find('#acceptedCount').text('0');
|
||||
modal.find('#pendingCount').text('0');
|
||||
modal.find('#rejectedCount').text('0');
|
||||
modal.find('#allPermissions').html('<div class="text-center py-4"><div class="spinner-border" role="status"></div></div>');
|
||||
modal.find('#acceptedPermissions').html('');
|
||||
modal.find('#pendingPermissions').html('');
|
||||
modal.find('#rejectedPermissions').html('');
|
||||
|
||||
// AJAX untuk mengambil detail izin
|
||||
$.ajax({
|
||||
url: '/toprank/user/' + userId + '/permissions',
|
||||
type: 'GET',
|
||||
data: {
|
||||
month: currentMonth,
|
||||
year: currentYear
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Update counter
|
||||
modal.find('#acceptedCount').text(response.stats.accepted);
|
||||
modal.find('#pendingCount').text(response.stats.pending);
|
||||
modal.find('#rejectedCount').text(response.stats.rejected);
|
||||
|
||||
// Render permissions
|
||||
renderPermissions(response.data.all, 'allPermissions');
|
||||
renderPermissions(response.data.accepted, 'acceptedPermissions');
|
||||
renderPermissions(response.data.pending, 'pendingPermissions');
|
||||
renderPermissions(response.data.rejected, 'rejectedPermissions');
|
||||
} else {
|
||||
modal.find('#allPermissions').html(`
|
||||
<div class="alert alert-danger">
|
||||
${response.message || 'Gagal memuat data izin'}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Terjadi kesalahan saat memuat data';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
modal.find('#allPermissions').html(`
|
||||
<div class="alert alert-danger">
|
||||
${errorMessage}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
|
@ -0,0 +1,324 @@
|
|||
@extends('dashboard.base')
|
||||
|
||||
@section('title', 'Ranking Abensi Karyawan')
|
||||
|
||||
@section('content')
|
||||
<div class="content-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-md-12 grid-margin">
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
|
||||
<h3 class="font-weight-bold">Ranking Karyawan Berdasarkan Absensi</h3>
|
||||
<h6 class="font-weight-normal mb-0" id="monthYearDisplay">
|
||||
Bulan: {{ $months[$currentMonth] }} {{ $currentYear }}
|
||||
<span class="text"> | Urutan: {{ $sort == 'desc' ? 'Terbanyak' : 'Tersedikit' }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-12 col-xl-4 text-right">
|
||||
<div class="dropdown d-inline mr-2">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="monthDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Pilih Bulan
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="monthDropdown">
|
||||
@foreach($months as $key => $month)
|
||||
<a class="dropdown-item month-select" href="#" data-month="{{ $key }}">{{ $month }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-inline">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="sortDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ $sort == 'desc' ? 'Terbanyak Izin' : 'Tersedikit Izin' }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="sortDropdown">
|
||||
<a class="dropdown-item sort-select" href="#" data-sort="desc">Terbanyak Izin</a>
|
||||
<a class="dropdown-item sort-select" href="#" data-sort="asc">Tersedikit Izin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ... (bagian tabel tetap sama) ... -->
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 grid-margin stretch-card">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Nama Karyawan</th>
|
||||
<th>Total Absen (in+out)</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rankingTableBody">
|
||||
@forelse($ranking as $index => $user)
|
||||
<tr>
|
||||
<td>{{ $ranking->firstItem() + $index }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->total_absen }}</td>
|
||||
<td>
|
||||
<a href="#" class="btn btn-sm btn-info view-detail"
|
||||
data-user-id="{{ $user->id }}"
|
||||
data-user-name="{{ $user->name }}">
|
||||
<i class="ti-eye"></i> Lihat
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">Tidak ada data absensi bulan ini</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<div class="mt-3">
|
||||
{{ $ranking->appends([
|
||||
'month' => $currentMonth,
|
||||
'year' => $currentYear,
|
||||
'sort' => $sort
|
||||
])->links() }}
|
||||
</div>
|
||||
|
||||
<h6 class="font-weight-normal mb-0" id="monthYearDisplay" style="display:none;">
|
||||
Bulan: {{ $months[$currentMonth] }} {{ $currentYear }}
|
||||
<span class="text"> | Urutan: {{ $sort == 'desc' ? 'Terbanyak' : 'Tersedikit' }}</span>
|
||||
</h6>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
{{ $ranking->appends([
|
||||
'month' => $currentMonth,
|
||||
'year' => $currentYear,
|
||||
'sort' => $sort
|
||||
])->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@endsection
|
||||
|
||||
<div class="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="detailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title" id="detailModalLabel">
|
||||
Detail Absensi: <span id="userName"></span>
|
||||
</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama</th>
|
||||
<th>Tanggal</th>
|
||||
<th>Jam</th>
|
||||
<th>Type</th>
|
||||
<th>Foto</th>
|
||||
<th>Latitude</th>
|
||||
<th>Longitude</th>
|
||||
<th>Lokasi</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="absenDetailBody">
|
||||
<tr><td colspan="9" class="text-center">Memuat data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@push('script')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let currentMonth = {{ $currentMonth }};
|
||||
let currentYear = {{ $currentYear }};
|
||||
let currentSort = '{{ $sort }}';
|
||||
|
||||
// Fungsi untuk memuat data ranking
|
||||
function loadRankingData() {
|
||||
$.ajax({
|
||||
url: '/toprank/absensi',
|
||||
type: 'GET',
|
||||
data: {
|
||||
month: currentMonth,
|
||||
year: currentYear,
|
||||
sort: currentSort
|
||||
},
|
||||
success: function(response) {
|
||||
$('#rankingTableBody').html($(response).find('#rankingTableBody').html());
|
||||
$('#monthYearDisplay').html($(response).find('#monthYearDisplay').html());
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('Error loading ranking data:', xhr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Handle pemilihan bulan
|
||||
$(document).on('click', '.month-select', function(e) {
|
||||
e.preventDefault();
|
||||
currentMonth = $(this).data('month');
|
||||
loadRankingData();
|
||||
});
|
||||
|
||||
// Handle pemilihan sorting
|
||||
$(document).on('click', '.sort-select', function(e) {
|
||||
e.preventDefault();
|
||||
currentSort = $(this).data('sort');
|
||||
loadRankingData();
|
||||
});
|
||||
|
||||
|
||||
// Fungsi untuk render permissions
|
||||
function renderPermissions(permissions, targetId) {
|
||||
let html = '<div class="list-group">';
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
permissions.forEach(function(permission) {
|
||||
let statusBadge = '';
|
||||
let statusClass = '';
|
||||
|
||||
if (permission.status === 'accepted') {
|
||||
statusBadge = 'Disetujui';
|
||||
statusClass = 'success';
|
||||
} else if (permission.status === 'pending') {
|
||||
statusBadge = 'Pending';
|
||||
statusClass = 'warning';
|
||||
} else {
|
||||
statusBadge = 'Ditolak';
|
||||
statusClass = 'danger';
|
||||
}
|
||||
|
||||
let proofPhoto = permission.proof_photo ?
|
||||
`<a href="/storage/permissions/${permission.proof_photo}" target="_blank" class="btn btn-sm btn-link mt-1">
|
||||
<i class="ti-image"></i> Lihat Bukti
|
||||
</a>` : '';
|
||||
|
||||
let approvedInfo = permission.approved_at ?
|
||||
`<small class="text-muted d-block mt-1">
|
||||
Disetujui pada: ${new Date(permission.approved_at).toLocaleDateString('id-ID')}
|
||||
</small>` : '';
|
||||
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1 font-weight-bold">${permission.category}</h6>
|
||||
<p class="mb-1">${permission.start_date} s/d ${permission.end_date}</p>
|
||||
<p class="mb-1 small">Alasan: ${permission.reason}</p>
|
||||
</div>
|
||||
<span class="badge badge-${statusClass}">${statusBadge}</span>
|
||||
</div>
|
||||
${proofPhoto}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Diajukan pada: ${new Date(permission.created_at).toLocaleDateString('id-ID')}
|
||||
</small>
|
||||
${approvedInfo}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += `
|
||||
<div class="list-group-item text-center py-4">
|
||||
<i class="ti-info-alt" style="font-size: 2rem;"></i>
|
||||
<p class="mt-2 mb-0">Tidak ada data izin</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('#' + targetId).html(html);
|
||||
}
|
||||
|
||||
// Handle modal detail
|
||||
$(document).on('click', '.view-detail', function(e) {
|
||||
e.preventDefault();
|
||||
const userId = $(this).data('user-id');
|
||||
const userName = $(this).data('user-name');
|
||||
|
||||
const modal = $('#detailModal');
|
||||
modal.find('#userName').text(userName);
|
||||
modal.modal('show');
|
||||
|
||||
$('#absenDetailBody').html('<tr><td colspan="9" class="text-center">Memuat data...</td></tr>');
|
||||
|
||||
$.ajax({
|
||||
url: '/toprank/absensi/user/' + userId + '/attendances',
|
||||
type: 'GET',
|
||||
data: {
|
||||
month: {{ $currentMonth }},
|
||||
year: {{ $currentYear }}
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
let html = '';
|
||||
if (response.data.length > 0) {
|
||||
response.data.forEach(function(absen) {
|
||||
const tanggal = new Date(absen.created_at).toLocaleDateString('id-ID');
|
||||
const jam = new Date(absen.created_at).toLocaleTimeString('id-ID');
|
||||
const foto = absen.photo ?
|
||||
`<a href="/storage/attendances/${absen.photo}" target="_blank">Lihat Foto</a>` : '-';
|
||||
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${absen.user.name}</td>
|
||||
<td>${tanggal}</td>
|
||||
<td>${jam}</td>
|
||||
<td>${absen.type.toUpperCase()}</td>
|
||||
<td>${foto}</td>
|
||||
<td>${absen.lat}</td>
|
||||
<td>${absen.long}</td>
|
||||
<td>${absen.location ? absen.location.name : '-'}</td>
|
||||
<td>${absen.status.replaceAll('_', ' ').toUpperCase()}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="9" class="text-center">Tidak ada data absensi</td></tr>';
|
||||
}
|
||||
$('#absenDetailBody').html(html);
|
||||
} else {
|
||||
$('#absenDetailBody').html(`<tr><td colspan="9" class="text-center text-danger">${response.message}</td></tr>`);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let message = 'Terjadi kesalahan saat memuat data';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
$('#absenDetailBody').html(`<tr><td colspan="9" class="text-center text-danger">${message}</td></tr>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
@endpush
|
|
@ -1,8 +1,12 @@
|
|||
<?php
|
||||
|
||||
|
||||
|
||||
use App\Http\Controllers\Api\Admin\ToprankController as AdminToprankController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\Auth\AuthController;
|
||||
use App\Http\Controllers\Api\Admin\RankingController as AdminRankingController;
|
||||
use App\Http\Controllers\Api\Admin\UserController as AdminUserController;
|
||||
use App\Http\Controllers\Api\Admin\AttendanceController as AdminAttendanceController;
|
||||
use App\Http\Controllers\Api\Admin\PermissionController as AdminPermissionController;
|
||||
|
@ -47,13 +51,17 @@ Route::name('api.')->group(function () {
|
|||
Route::get('/attendances/{id}', [AdminAttendanceController::class, 'show']);
|
||||
Route::put('/attendances/{id}/approve', [AdminAttendanceController::class, 'approve']);
|
||||
Route::get('/attendances/data/statistics', [AdminAttendanceController::class, 'statistics']);
|
||||
|
||||
Route::delete('/attendances/{id}', [AdminAttendanceController::class, 'destroy']);
|
||||
|
||||
Route::get('/permissions', [AdminPermissionController::class, 'index']);
|
||||
Route::get('/permissions/{id}', [AdminPermissionController::class, 'show']);
|
||||
Route::put('/permissions/{id}/approve', [AdminPermissionController::class, 'approve']);
|
||||
Route::get('/permissions/data/statistics', [AdminPermissionController::class, 'statistics']);
|
||||
|
||||
// Route::get('/ranking/permissions/user/{id}', [AdminToprankController::class, 'userPermissionsWeb']);
|
||||
Route::get('/toprank/absensi', [AdminToprankController::class, 'absensiIndex']);
|
||||
Route::get('/toprank/absensi/user/{userId}/attendances', [AdminToprankController::class, 'userAttendances']);
|
||||
|
||||
Route::get('/locations', [AdminLocationController::class, 'index']);
|
||||
Route::post('/locations', [AdminLocationController::class, 'store']);
|
||||
Route::get('/locations/{id}', [AdminLocationController::class, 'show']);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\Admin\RankingController as AdminRankingController;
|
||||
use App\Http\Controllers\api\admin\ToprankController as AdminToprankController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Web\Admin\UserController;
|
||||
use App\Http\Controllers\Web\Admin\AttendanceController;
|
||||
use App\Http\Controllers\Web\Admin\PermissionController;
|
||||
use App\Http\Controllers\Web\Admin\LocationController;
|
||||
use App\Http\Controllers\Web\Admin\ReportController;
|
||||
use App\Http\Controllers\Web\Admin\RankingController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -47,4 +50,18 @@ Route::middleware(['web.auth:admin'])->group(function () {
|
|||
Route::prefix('reports')->name('reports.')->group(function () {
|
||||
Route::get('/', fn()=>view('dashboard.report'))->name('index');
|
||||
});
|
||||
|
||||
Route::prefix('ranking')->name('ranking.')->group(function () {
|
||||
Route::get('/', fn()=>view('dashboard.ranking'))->name('index');
|
||||
});
|
||||
|
||||
Route::prefix('toprank')->name('toprank.')->group(function () {
|
||||
// ini untuk izin
|
||||
Route::get('/', [AdminToprankController::class, 'index'])->name('index');
|
||||
Route::get('/izin', [AdminToprankController::class, 'index'])->name('izin');
|
||||
Route::get('/user/{id}/permissions', [AdminToprankController::class, 'userPermissionsWeb'])->name('toprank.user_permissions');
|
||||
// ini untuk absen
|
||||
Route::get('/absensi', [AdminToprankController::class, 'absensi'])->name('absensi');
|
||||
Route::get('/absensi/user/{userId}/attendances', [AdminToprankController::class, 'userAttendances'])->name('absensi.attendances');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInita29a979da9754defe6e0c8ead1270f52::getLoader();
|
|
@ -0,0 +1,126 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
[3.0.0]
|
||||
Version 3.x supports DomPDF version 3.x. See the changelog in https://github.com/dompdf/dompdf/releases/tag/v3.0.0
|
||||
|
||||
The most notable change in laravel-dompdf are the changed defaults, to be more secure;
|
||||
- `enable_remote` is now `false` by default. Change with caution.
|
||||
- `allowedRemoteHosts` and `artifactPathValidation` are added the the config.
|
||||
Also, support for Laravel < 9 and PHP < 8.1 is dropped.
|
||||
|
||||
## [3.1]
|
||||
This release updates the config for [dompdf/dompdf v3.1.0](https://github.com/dompdf/dompdf/releases/tag/v3.1.0) which contains the. following breaking URL:
|
||||
|
||||
> **Breaking Change**
|
||||
> This release adds the "data://" scheme to the protocol validation rules. Installations that explicitly define the allowed protocols but do not include the "data://" protocol will no longer render data-URIs. This is a change from previous versions, where data-URIs were not processed through the validated rules. Installations that use the default validation rules included with Dompdf should see no impact.
|
||||
|
||||
The update for laravel-dompdf adds this to the default config, but if you have published the config, you need to add the `data://` scheme.
|
||||
|
||||
## [3.0]
|
||||
|
||||
Version 3.x supports DomPDF version 3.x. See the changelog in https://github.com/dompdf/dompdf/releases/tag/v3.0.0
|
||||
|
||||
The most notable change in laravel-dompdf are the changed defaults, to be more secure;
|
||||
|
||||
enable_remote is now false by default. Change with caution.
|
||||
allowedRemoteHosts and artifactPathValidation are added the the config.
|
||||
Also, support for Laravel < 9 and PHP < 8.1 is dropped.
|
||||
|
||||
## [2.2.0]
|
||||
### What's Changed
|
||||
* Fix setOptions by @cesarreyes3 in https://github.com/barryvdh/laravel-dompdf/pull/1040
|
||||
* Bump dompdf minimum to 2.0.7 by @barryvdh
|
||||
|
||||
## New Contributors
|
||||
* @cesarreyes3 made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/1040
|
||||
|
||||
**Full Changelog**: https://github.com/barryvdh/laravel-dompdf/compare/v2.1.1...v2.2.0
|
||||
|
||||
## [2.1.1]
|
||||
### What's Changed
|
||||
* Revert "Fix setOptions method" by @barryvdh in https://github.com/barryvdh/laravel-dompdf/pull/1039
|
||||
|
||||
**Full Changelog**: https://github.com/barryvdh/laravel-dompdf/compare/v2.1.0...v2.1.1
|
||||
|
||||
## [2.1.0]
|
||||
### What's Changed
|
||||
* Convert phpunit by @barryvdh in https://github.com/barryvdh/laravel-dompdf/pull/952
|
||||
* ci: Use GitHub Actions V3 by @DannyvdSluijs in https://github.com/barryvdh/laravel-dompdf/pull/990
|
||||
* Fix named arguments when using facade by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/1002
|
||||
* Update dompdf version as a dependancy by @AliSheikhDev in https://github.com/barryvdh/laravel-dompdf/pull/967
|
||||
* ci: Use GitHub Actions V4 by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/1003
|
||||
* Fix phpstan analysis by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/972
|
||||
* Fix setOptions method by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/974
|
||||
* Small typo fix in dompdf config file by @ricklambrechts in https://github.com/barryvdh/laravel-dompdf/pull/1004
|
||||
* Upgrade to larastan/larastan by @parth391 in https://github.com/barryvdh/laravel-dompdf/pull/1014
|
||||
* Fixing "Upgrade to larastan/larastan" by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/1018
|
||||
* Laravel 11 Support by @erikn69 in https://github.com/barryvdh/laravel-dompdf/pull/1036
|
||||
* Laravel 11.x Compatibility by @laravel-shift in https://github.com/barryvdh/laravel-dompdf/pull/1037
|
||||
|
||||
### New Contributors
|
||||
* @DannyvdSluijs made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/990
|
||||
* @AliSheikhDev made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/967
|
||||
* @ricklambrechts made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/1004
|
||||
* @parth391 made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/1014
|
||||
* @laravel-shift made their first contribution in https://github.com/barryvdh/laravel-dompdf/pull/1037
|
||||
|
||||
**Full Changelog**: https://github.com/barryvdh/laravel-dompdf/compare/v2.0.1...v2.0.2
|
||||
|
||||
## [2.0.0]
|
||||
|
||||
Version 2 supports DomPDF 2.x
|
||||
|
||||
### Changed
|
||||
- Remove the deprecated class 'Barryvdh\DomPDF\Facade' Facade in favor of Barryvdh\DomPDF\Facade\Pdf
|
||||
- Set default Facade to Pdf instead of PDF
|
||||
- HTML5 parser option is deprecated, because this is always on.
|
||||
- `orientation` option was never used. Removed in favor of `options.default_paper_orientation`
|
||||
|
||||
### Added
|
||||
- Upgraded to use dompdf/dompdf 2.x
|
||||
- `setOption` to change only the specified option(s), instead of replace all options.
|
||||
- Magic methods to allow calls to Dompdf methods easier. (#892)
|
||||
- `default_paper_orientation` option has been added to the defaults.
|
||||
- Add option to set public path (#890)
|
||||
|
||||
### Deprecated
|
||||
- `setOptions` is now deprecated. Use `setOption` instead.
|
||||
- Config `dompdf.defines` has been renamed to `dompdf.options`
|
||||
|
||||
|
||||
## [2.0.0-beta3]
|
||||
### Changed
|
||||
- Remove the deprecated class 'Barryvdh\DomPDF\Facade' Facade in favor of Barryvdh\DomPDF\Facade\Pdf
|
||||
- Set default Facade to Pdf instead of PDF
|
||||
|
||||
## [2.0.0-beta2]
|
||||
|
||||
### Added
|
||||
- Upgraded to use dompdf/dompdf 2.x
|
||||
- `setOption` to change only the specified option(s), instead of replace all options.
|
||||
- Magic methods to allow calls to Dompdf methods easier. (#892)
|
||||
- `default_paper_orientation` option has been added to the defaults.
|
||||
- Add option to set public path (#890)
|
||||
|
||||
### Changed
|
||||
- HTML5 parser option is deprecated, because this is always on.
|
||||
- `orientation` option was never used. Removed in favor of `options.default_paper_orientation`
|
||||
|
||||
### Deprecated
|
||||
- `setOptions` is now deprecated. Use `setOption` instead.
|
||||
- Config `dompdf.defines` has been renamed to `dompdf.options`
|
||||
|
||||
|
||||
## Dompdf 2.0.0, highlights since 1.2.x
|
||||
> https://github.com/dompdf/dompdf/releases/tag/v2.0.0
|
||||
> - Addresses multiple security vulnerabilities (see link)
|
||||
> - Modifies callback and page_script/page_text handling (breaking change, see link)
|
||||
> - Switches the HTML5 parser to Masterminds/HTML5
|
||||
> - Improves CSS property parsing and representation
|
||||
> - Improves border, outline, and background rendering for inline elements
|
||||
> - Switches installed fonts and font metrics cache file format to JSON
|
||||
> - Adds support for the inset CSS shorthand property and the legacy break-word keyword for word-break
|
||||
> - Adds "end_document" callback event
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 barryvdh
|
||||
|
||||
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,64 @@
|
|||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
"description": "A DOMPDF Wrapper for Laravel",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"dompdf",
|
||||
"pdf"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"illuminate/support": "^9|^10|^11|^12"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^7|^8|^9|^10",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"phpro/grumphp": "^2.5",
|
||||
"larastan/larastan": "^2.7|^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\DomPDF\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\DomPDF\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"check-style": "phpcs -p --standard=psr12 src/",
|
||||
"fix-style": "phpcbf -p --standard=psr12 src/",
|
||||
"phpstan": "phpstan analyze --memory-limit=-1"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"phpro/grumphp": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set some default values. It is possible to add all defines that can be set
|
||||
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||
|
|
||||
*/
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
|
||||
'public_path' => null, // Override the public path if needed
|
||||
|
||||
/*
|
||||
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
|
||||
*/
|
||||
'convert_entities' => true,
|
||||
|
||||
'options' => [
|
||||
/**
|
||||
* The location of the DOMPDF font directory
|
||||
*
|
||||
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
* *Please note the trailing slash.*
|
||||
*
|
||||
* Notes regarding fonts:
|
||||
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||
*
|
||||
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||
* review your rights under the font license.
|
||||
*
|
||||
* Any font specification in the source HTML is translated to the closest font available
|
||||
* in the font directory.
|
||||
*
|
||||
* The pdf standard "Base 14 fonts" are:
|
||||
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
*
|
||||
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||
* This directory can be the same as DOMPDF_FONT_DIR
|
||||
*
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
*/
|
||||
'font_cache' => storage_path('fonts'),
|
||||
|
||||
/**
|
||||
* The location of a temporary directory.
|
||||
*
|
||||
* The directory specified must be writeable by the webserver process.
|
||||
* The temporary directory is required to download remote images and when
|
||||
* using the PDFLib back end.
|
||||
*/
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
|
||||
/**
|
||||
* ==== IMPORTANT ====
|
||||
*
|
||||
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||
* files on the webserver. All local files opened by dompdf must be in a
|
||||
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||
* allow an attacker to use dompdf to read any files on the server. This
|
||||
* should be an absolute path.
|
||||
* This is only checked on command line call by dompdf.php, but not by
|
||||
* direct class use like:
|
||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||
*/
|
||||
'chroot' => realpath(base_path()),
|
||||
|
||||
/**
|
||||
* Protocol whitelist
|
||||
*
|
||||
* Protocols and PHP wrappers allowed in URIs, and the validation rules
|
||||
* that determine if a resouce may be loaded. Full support is not guaranteed
|
||||
* for the protocols/wrappers specified
|
||||
* by this array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
'allowed_protocols' => [
|
||||
'data://' => ['rules' => []],
|
||||
'file://' => ['rules' => []],
|
||||
'http://' => ['rules' => []],
|
||||
'https://' => ['rules' => []],
|
||||
],
|
||||
|
||||
/**
|
||||
* Operational artifact (log files, temporary files) path validation
|
||||
*/
|
||||
'artifactPathValidation' => null,
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
'log_output_file' => null,
|
||||
|
||||
/**
|
||||
* Whether to enable font subsetting or not.
|
||||
*/
|
||||
'enable_font_subsetting' => false,
|
||||
|
||||
/**
|
||||
* The PDF rendering backend to use
|
||||
*
|
||||
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||
* fall back on CPDF. 'GD' renders PDFs to graphic files.
|
||||
* {@link * Canvas_Factory} ultimately determines which rendering class to
|
||||
* instantiate based on this setting.
|
||||
*
|
||||
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||
* capabilities for dompdf, however additional features (e.g. object,
|
||||
* image and font support, etc.) differ between backends. Please see
|
||||
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||
* on CPDF. Also see the documentation for each backend at the links
|
||||
* below.
|
||||
*
|
||||
* The GD rendering backend is a little different than PDFLib and
|
||||
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||
* not make any sense when creating image files. For example,
|
||||
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||
* look at {@link GD_Adapter} for more information. GD support is
|
||||
* experimental, so use it at your own risk.
|
||||
*
|
||||
* @link http://www.pdflib.com
|
||||
* @link http://www.ros.co.nz/pdf
|
||||
* @link http://www.php.net/image
|
||||
*/
|
||||
'pdf_backend' => 'CPDF',
|
||||
|
||||
/**
|
||||
* html target media view which should be rendered into pdf.
|
||||
* List of types and parsing rules for future extensions:
|
||||
* http://www.w3.org/TR/REC-html40/types.html
|
||||
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||
* Note, even though the generated pdf file is intended for print output,
|
||||
* the desired content might be different (e.g. screen or projection view of html file).
|
||||
* Therefore allow specification of content here.
|
||||
*/
|
||||
'default_media_type' => 'screen',
|
||||
|
||||
/**
|
||||
* The default paper size.
|
||||
*
|
||||
* North America standard is "letter"; other countries generally "a4"
|
||||
*
|
||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||
*/
|
||||
'default_paper_size' => 'a4',
|
||||
|
||||
/**
|
||||
* The default paper orientation.
|
||||
*
|
||||
* The orientation of the page (portrait or landscape).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
'default_paper_orientation' => 'portrait',
|
||||
|
||||
/**
|
||||
* The default font family
|
||||
*
|
||||
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
'default_font' => 'serif',
|
||||
|
||||
/**
|
||||
* Image DPI setting
|
||||
*
|
||||
* This setting determines the default DPI setting for images and fonts. The
|
||||
* DPI may be overridden for inline images by explictly setting the
|
||||
* image's width & height style attributes (i.e. if the image's native
|
||||
* width is 600 pixels and you specify the image's width as 72 points,
|
||||
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||
* background images can not be overridden and is controlled entirely
|
||||
* via this parameter.
|
||||
*
|
||||
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||
* If a size in html is given as px (or without unit as image size),
|
||||
* this tells the corresponding size in pt.
|
||||
* This adjusts the relative sizes to be similar to the rendering of the
|
||||
* html page in a reference browser.
|
||||
*
|
||||
* In pdf, always 1 pt = 1/72 inch
|
||||
*
|
||||
* Rendering resolution of various browsers in px per inch:
|
||||
* Windows Firefox and Internet Explorer:
|
||||
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||
* Linux Firefox:
|
||||
* about:config *resolution: Default:96
|
||||
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||
*
|
||||
* Take care about extra font/image zoom factor of browser.
|
||||
*
|
||||
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||
* the real image dimension in px for rendering.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
'dpi' => 96,
|
||||
|
||||
/**
|
||||
* Enable embedded PHP
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
|
||||
* within <script type="text/php"> ... </script> tags.
|
||||
*
|
||||
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
|
||||
* is a security risk.
|
||||
* Embedded scripts are run with the same level of system access available to dompdf.
|
||||
* Set this option to false (recommended) if you wish to process untrusted documents.
|
||||
* This setting may increase the risk of system exploit.
|
||||
* Do not change this settings without understanding the consequences.
|
||||
* Additional documentation is available on the dompdf wiki at:
|
||||
* https://github.com/dompdf/dompdf/wiki
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
'enable_php' => false,
|
||||
|
||||
/**
|
||||
* Rnable inline JavaScript
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
|
||||
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
|
||||
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
|
||||
* not browser-based JavaScript executed by Dompdf.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
'enable_javascript' => true,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
*
|
||||
* If this setting is set to true, DOMPDF will access remote sites for
|
||||
* images and CSS files as required.
|
||||
*
|
||||
* ==== IMPORTANT ====
|
||||
* This can be a security risk, in particular in combination with isPhpEnabled and
|
||||
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
|
||||
* This allows anonymous users to download legally doubtful internet content which on
|
||||
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||
* in remote html pages to be executed by your server with your account privileges.
|
||||
*
|
||||
* This setting may increase the risk of system exploit. Do not change
|
||||
* this settings without understanding the consequences. Additional
|
||||
* documentation is available on the dompdf wiki at:
|
||||
* https://github.com/dompdf/dompdf/wiki
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
'enable_remote' => false,
|
||||
|
||||
/**
|
||||
* List of allowed remote hosts
|
||||
*
|
||||
* Each value of the array must be a valid hostname.
|
||||
*
|
||||
* This will be used to filter which resources can be loaded in combination with
|
||||
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
|
||||
*
|
||||
* Leave to NULL to allow any remote host.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
'allowed_remote_hosts' => null,
|
||||
|
||||
/**
|
||||
* A ratio applied to the fonts height to be more like browsers' line height
|
||||
*/
|
||||
'font_height_ratio' => 1.1,
|
||||
|
||||
/**
|
||||
* Use the HTML5 Lib parser
|
||||
*
|
||||
* @deprecated This feature is now always on in dompdf 2.x
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
'enable_html5_parser' => true,
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,134 @@
|
|||
## DOMPDF Wrapper for Laravel
|
||||
|
||||
### Laravel wrapper for [Dompdf HTML to PDF Converter](https://github.com/dompdf/dompdf)
|
||||
|
||||
[](https://github.com/barryvdh/laravel-dompdf/actions)
|
||||
[](http://choosealicense.com/licenses/mit/)
|
||||
[](https://packagist.org/packages/barryvdh/laravel-dompdf)
|
||||
[](https://packagist.org/packages/barryvdh/laravel-dompdf)
|
||||
[](https://fruitcake.nl/)
|
||||
|
||||
## Installation
|
||||
|
||||
### Laravel
|
||||
Require this package in your composer.json and update composer. This will download the package and the dompdf + fontlib libraries also.
|
||||
|
||||
composer require barryvdh/laravel-dompdf
|
||||
|
||||
### Lumen
|
||||
|
||||
After updating composer add the following lines to register provider in `bootstrap/app.php`
|
||||
|
||||
```
|
||||
$app->register(\Barryvdh\DomPDF\ServiceProvider::class);
|
||||
```
|
||||
|
||||
To change the configuration, copy the config file to your config folder and enable it in `bootstrap/app.php`:
|
||||
|
||||
```
|
||||
$app->configure('dompdf');
|
||||
```
|
||||
|
||||
## Using
|
||||
|
||||
You can create a new DOMPDF instance and load a HTML string, file or view name. You can save it to a file, or stream (show in browser) or download.
|
||||
|
||||
```php
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
$pdf = Pdf::loadView('pdf.invoice', $data);
|
||||
return $pdf->download('invoice.pdf');
|
||||
```
|
||||
|
||||
or use the App container:
|
||||
|
||||
```php
|
||||
$pdf = App::make('dompdf.wrapper');
|
||||
$pdf->loadHTML('<h1>Test</h1>');
|
||||
return $pdf->stream();
|
||||
```
|
||||
|
||||
Or use the facade:
|
||||
|
||||
You can chain the methods:
|
||||
|
||||
```php
|
||||
return Pdf::loadFile(public_path().'/myfile.html')->save('/path-to/my_stored_file.pdf')->stream('download.pdf');
|
||||
```
|
||||
|
||||
You can change the orientation and paper size, and hide or show errors (by default, errors are shown when debug is on)
|
||||
|
||||
```php
|
||||
Pdf::loadHTML($html)->setPaper('a4', 'landscape')->setWarnings(false)->save('myfile.pdf')
|
||||
```
|
||||
|
||||
If you need the output as a string, you can get the rendered PDF with the output() function, so you can save/output it yourself.
|
||||
|
||||
Use `php artisan vendor:publish` to create a config file located at `config/dompdf.php` which will allow you to define local configurations to change some settings (default paper etc).
|
||||
You can also use your ConfigProvider to set certain keys.
|
||||
|
||||
### Configuration
|
||||
The defaults configuration settings are set in `config/dompdf.php`. Copy this file to your own config directory to modify the values. You can publish the config using this command:
|
||||
```shell
|
||||
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"
|
||||
```
|
||||
|
||||
You can still alter the dompdf options in your code before generating the pdf using this command:
|
||||
```php
|
||||
Pdf::setOption(['dpi' => 150, 'defaultFont' => 'sans-serif']);
|
||||
```
|
||||
|
||||
Available options and their defaults:
|
||||
* __rootDir__: "{app_directory}/vendor/dompdf/dompdf"
|
||||
* __tempDir__: "/tmp" _(available in config/dompdf.php)_
|
||||
* __fontDir__: "{app_directory}/storage/fonts" _(available in config/dompdf.php)_
|
||||
* __fontCache__: "{app_directory}/storage/fonts" _(available in config/dompdf.php)_
|
||||
* __chroot__: "{app_directory}" _(available in config/dompdf.php)_
|
||||
* __logOutputFile__: "/tmp/log.htm"
|
||||
* __defaultMediaType__: "screen" _(available in config/dompdf.php)_
|
||||
* __defaultPaperSize__: "a4" _(available in config/dompdf.php)_
|
||||
* __defaultFont__: "serif" _(available in config/dompdf.php)_
|
||||
* __dpi__: 96 _(available in config/dompdf.php)_
|
||||
* __fontHeightRatio__: 1.1 _(available in config/dompdf.php)_
|
||||
* __isPhpEnabled__: false _(available in config/dompdf.php)_
|
||||
* __isRemoteEnabled__: false _(available in config/dompdf.php)_
|
||||
* __isJavascriptEnabled__: true _(available in config/dompdf.php)_
|
||||
* __isHtml5ParserEnabled__: true _(available in config/dompdf.php)_
|
||||
* __allowedRemoteHosts__: null _(available in config/dompdf.php)_
|
||||
* __isFontSubsettingEnabled__: false _(available in config/dompdf.php)_
|
||||
* __debugPng__: false
|
||||
* __debugKeepTemp__: false
|
||||
* __debugCss__: false
|
||||
* __debugLayout__: false
|
||||
* __debugLayoutLines__: true
|
||||
* __debugLayoutBlocks__: true
|
||||
* __debugLayoutInline__: true
|
||||
* __debugLayoutPaddingBox__: true
|
||||
* __pdfBackend__: "CPDF" _(available in config/dompdf.php)_
|
||||
* __pdflibLicense__: ""
|
||||
* __adminUsername__: "user"
|
||||
* __adminPassword__: "password"
|
||||
* __artifactPathValidation__: null _(available in config/dompdf.php)_
|
||||
|
||||
#### Note: Since 3.x the remote access is disabled by default, to provide more security. Use with caution!
|
||||
|
||||
### Tip: UTF-8 support
|
||||
In your templates, set the UTF-8 Metatag:
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
|
||||
### Tip: Page breaks
|
||||
You can use the CSS `page-break-before`/`page-break-after` properties to create a new page.
|
||||
|
||||
<style>
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
</style>
|
||||
<h1>Page 1</h1>
|
||||
<div class="page-break"></div>
|
||||
<h1>Page 2</h1>
|
||||
|
||||
### License
|
||||
|
||||
This DOMPDF Wrapper for Laravel is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Barryvdh\DomPDF\Facade;
|
||||
|
||||
use Barryvdh\DomPDF\PDF as BasePDF;
|
||||
use Illuminate\Support\Facades\Facade as IlluminateFacade;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @method static BasePDF setBaseHost(string $baseHost)
|
||||
* @method static BasePDF setBasePath(string $basePath)
|
||||
* @method static BasePDF setCanvas(\Dompdf\Canvas $canvas)
|
||||
* @method static BasePDF setCallbacks(array<string, mixed> $callbacks)
|
||||
* @method static BasePDF setCss(\Dompdf\Css\Stylesheet $css)
|
||||
* @method static BasePDF setDefaultView(string $defaultView, array<string, mixed> $options)
|
||||
* @method static BasePDF setDom(\DOMDocument $dom)
|
||||
* @method static BasePDF setFontMetrics(\Dompdf\FontMetrics $fontMetrics)
|
||||
* @method static BasePDF setHttpContext(resource|array<string, mixed> $httpContext)
|
||||
* @method static BasePDF setPaper(string|float[] $paper, string $orientation = 'portrait')
|
||||
* @method static BasePDF setProtocol(string $protocol)
|
||||
* @method static BasePDF setTree(\Dompdf\Frame\FrameTree $tree)
|
||||
* @method static BasePDF setWarnings(bool $warnings)
|
||||
* @method static BasePDF setOption(array<string, mixed>|string $attribute, $value = null)
|
||||
* @method static BasePDF setOptions(array<string, mixed> $options)
|
||||
* @method static BasePDF loadView(string $view, array<string, mixed> $data = [], array<string, mixed> $mergeData = [], ?string $encoding = null)
|
||||
* @method static BasePDF loadHTML(string $string, ?string $encoding = null)
|
||||
* @method static BasePDF loadFile(string $file)
|
||||
* @method static BasePDF addInfo(array<string, string> $info)
|
||||
* @method static string output(array<string, int> $options = [])
|
||||
* @method static BasePDF save()
|
||||
* @method static \Illuminate\Http\Response download(string $filename = 'document.pdf')
|
||||
* @method static \Illuminate\Http\Response stream(string $filename = 'document.pdf')
|
||||
*/
|
||||
class Pdf extends IlluminateFacade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'dompdf.wrapper';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic, static calls to the object.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array<mixed> $args
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function __callStatic($method, $args)
|
||||
{
|
||||
/** @var \Illuminate\Contracts\Foundation\Application|null */
|
||||
$app = static::getFacadeApplication();
|
||||
if (! $app) {
|
||||
throw new RuntimeException('Facade application has not been set.');
|
||||
}
|
||||
|
||||
// Resolve a new instance, avoid using a cached instance
|
||||
$instance = $app->make(static::getFacadeAccessor());
|
||||
|
||||
return $instance->$method(...$args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
<?php
|
||||
|
||||
namespace Barryvdh\DomPDF;
|
||||
|
||||
use Dompdf\Adapter\CPDF;
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Exception;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
|
||||
/**
|
||||
* A Laravel wrapper for Dompdf
|
||||
*
|
||||
* @package laravel-dompdf
|
||||
* @author Barry vd. Heuvel
|
||||
*
|
||||
* @method PDF setBaseHost(string $baseHost)
|
||||
* @method PDF setBasePath(string $basePath)
|
||||
* @method PDF setCanvas(\Dompdf\Canvas $canvas)
|
||||
* @method PDF setCallbacks(array<string, mixed> $callbacks)
|
||||
* @method PDF setCss(\Dompdf\Css\Stylesheet $css)
|
||||
* @method PDF setDefaultView(string $defaultView, array<string, mixed> $options)
|
||||
* @method PDF setDom(\DOMDocument $dom)
|
||||
* @method PDF setFontMetrics(\Dompdf\FontMetrics $fontMetrics)
|
||||
* @method PDF setHttpContext(resource|array<string, mixed> $httpContext)
|
||||
* @method PDF setPaper(string|float[] $paper, string $orientation = 'portrait')
|
||||
* @method PDF setProtocol(string $protocol)
|
||||
* @method PDF setTree(\Dompdf\Frame\FrameTree $tree)
|
||||
* @method string getBaseHost()
|
||||
* @method string getBasePath()
|
||||
* @method \Dompdf\Canvas getCanvas()
|
||||
* @method array<string, mixed> getCallbacks()
|
||||
* @method \Dompdf\Css\Stylesheet getCss()
|
||||
* @method \DOMDocument getDom()
|
||||
* @method \Dompdf\FontMetrics getFontMetrics()
|
||||
* @method resource getHttpContext()
|
||||
* @method Options getOptions()
|
||||
* @method \Dompdf\Frame\FrameTree getTree()
|
||||
* @method string getPaperOrientation()
|
||||
* @method float[] getPaperSize()
|
||||
* @method string getProtocol()
|
||||
*/
|
||||
class PDF
|
||||
{
|
||||
/** @var Dompdf */
|
||||
protected $dompdf;
|
||||
|
||||
/** @var \Illuminate\Contracts\Config\Repository */
|
||||
protected $config;
|
||||
|
||||
/** @var \Illuminate\Filesystem\Filesystem */
|
||||
protected $files;
|
||||
|
||||
/** @var \Illuminate\Contracts\View\Factory */
|
||||
protected $view;
|
||||
|
||||
/** @var bool */
|
||||
protected $rendered = false;
|
||||
|
||||
/** @var bool */
|
||||
protected $showWarnings;
|
||||
|
||||
/** @var string */
|
||||
protected $public_path;
|
||||
|
||||
public function __construct(Dompdf $dompdf, ConfigRepository $config, Filesystem $files, ViewFactory $view)
|
||||
{
|
||||
$this->dompdf = $dompdf;
|
||||
$this->config = $config;
|
||||
$this->files = $files;
|
||||
$this->view = $view;
|
||||
|
||||
$this->showWarnings = $this->config->get('dompdf.show_warnings', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DomPDF instance
|
||||
*/
|
||||
public function getDomPDF(): Dompdf
|
||||
{
|
||||
return $this->dompdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide warnings
|
||||
*/
|
||||
public function setWarnings(bool $warnings): self
|
||||
{
|
||||
$this->showWarnings = $warnings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a HTML string
|
||||
*
|
||||
* @param string|null $encoding Not used yet
|
||||
*/
|
||||
public function loadHTML(string $string, ?string $encoding = null): self
|
||||
{
|
||||
$string = $this->convertEntities($string);
|
||||
$this->dompdf->loadHtml($string, $encoding);
|
||||
$this->rendered = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a HTML file
|
||||
*/
|
||||
public function loadFile(string $file): self
|
||||
{
|
||||
$this->dompdf->loadHtmlFile($file);
|
||||
$this->rendered = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata info
|
||||
* @param array<string, string> $info
|
||||
*/
|
||||
public function addInfo(array $info): self
|
||||
{
|
||||
foreach ($info as $name => $value) {
|
||||
$this->dompdf->add_info($name, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a View and convert to HTML
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $mergeData
|
||||
* @param string|null $encoding Not used yet
|
||||
*/
|
||||
public function loadView(string $view, array $data = [], array $mergeData = [], ?string $encoding = null): self
|
||||
{
|
||||
$html = $this->view->make($view, $data, $mergeData)->render();
|
||||
return $this->loadHTML($html, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set/Change an option (or array of options) in Dompdf
|
||||
*
|
||||
* @param array<string, mixed>|string $attribute
|
||||
* @param null|mixed $value
|
||||
*/
|
||||
public function setOption($attribute, $value = null): self
|
||||
{
|
||||
$this->dompdf->getOptions()->set($attribute, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all the Options from DomPDF
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function setOptions(array $options, bool $mergeWithDefaults = false): self
|
||||
{
|
||||
if ($mergeWithDefaults) {
|
||||
$options = array_merge(app()->make('dompdf.options'), $options);
|
||||
}
|
||||
|
||||
$this->dompdf->setOptions(new Options($options));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the PDF as a string.
|
||||
*
|
||||
* The options parameter controls the output. Accepted options are:
|
||||
*
|
||||
* 'compress' = > 1 or 0 - apply content stream compression, this is
|
||||
* on (1) by default
|
||||
*
|
||||
* @param array<string, int> $options
|
||||
*
|
||||
* @return string The rendered PDF as string
|
||||
*/
|
||||
public function output(array $options = []): string
|
||||
{
|
||||
if (!$this->rendered) {
|
||||
$this->render();
|
||||
}
|
||||
return (string) $this->dompdf->output($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the PDF to a file
|
||||
*/
|
||||
public function save(string $filename, ?string $disk = null): self
|
||||
{
|
||||
$disk = $disk ?: $this->config->get('dompdf.disk');
|
||||
|
||||
if (! is_null($disk)) {
|
||||
Storage::disk($disk)->put($filename, $this->output());
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->files->put($filename, $this->output());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the PDF downloadable by the user
|
||||
*/
|
||||
public function download(string $filename = 'document.pdf'): Response
|
||||
{
|
||||
$output = $this->output();
|
||||
$fallback = $this->fallbackName($filename);
|
||||
|
||||
return new Response($output, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => HeaderUtils::makeDisposition('attachment', $filename, $fallback),
|
||||
'Content-Length' => strlen($output),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a response with the PDF to show in the browser
|
||||
*/
|
||||
public function stream(string $filename = 'document.pdf'): Response
|
||||
{
|
||||
$output = $this->output();
|
||||
$fallback = $this->fallbackName($filename);
|
||||
|
||||
|
||||
return new Response($output, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => HeaderUtils::makeDisposition('inline', $filename, $fallback),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PDF
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
$this->dompdf->render();
|
||||
|
||||
if ($this->showWarnings) {
|
||||
global $_dompdf_warnings;
|
||||
if (!empty($_dompdf_warnings) && count($_dompdf_warnings)) {
|
||||
$warnings = '';
|
||||
foreach ($_dompdf_warnings as $msg) {
|
||||
$warnings .= $msg . "\n";
|
||||
}
|
||||
// $warnings .= $this->dompdf->get_canvas()->get_cpdf()->messages;
|
||||
if (!empty($warnings)) {
|
||||
throw new Exception($warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->rendered = true;
|
||||
}
|
||||
|
||||
/** @param array<string> $pc */
|
||||
public function setEncryption(string $password, string $ownerpassword = '', array $pc = []): void
|
||||
{
|
||||
$this->render();
|
||||
$canvas = $this->dompdf->getCanvas();
|
||||
if (! $canvas instanceof CPDF) {
|
||||
throw new \RuntimeException('Encryption is only supported when using CPDF');
|
||||
}
|
||||
$canvas->get_cpdf()->setEncryption($password, $ownerpassword, $pc);
|
||||
}
|
||||
|
||||
protected function convertEntities(string $subject): string
|
||||
{
|
||||
if (false === $this->config->get('dompdf.convert_entities', true)) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$entities = [
|
||||
'€' => '€',
|
||||
'£' => '£',
|
||||
];
|
||||
|
||||
foreach ($entities as $search => $replace) {
|
||||
$subject = str_replace($search, $replace, $subject);
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically handle calls into the dompdf instance.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array<mixed> $parameters
|
||||
* @return $this|mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->$method(...$parameters);
|
||||
}
|
||||
|
||||
if (method_exists($this->dompdf, $method)) {
|
||||
$return = $this->dompdf->$method(...$parameters);
|
||||
|
||||
return $return == $this->dompdf ? $this : $return;
|
||||
}
|
||||
|
||||
throw new \UnexpectedValueException("Method [{$method}] does not exist on PDF instance.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a safe fallback filename
|
||||
*/
|
||||
protected function fallbackName(string $filename): string
|
||||
{
|
||||
return str_replace('%', '', Str::ascii($filename));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace Barryvdh\DomPDF;
|
||||
|
||||
use Dompdf\Dompdf;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
|
||||
|
||||
class ServiceProvider extends IlluminateServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = false;
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$configPath = __DIR__ . '/../config/dompdf.php';
|
||||
$this->mergeConfigFrom($configPath, 'dompdf');
|
||||
|
||||
$this->app->bind('dompdf.options', function ($app) {
|
||||
$defines = $app['config']->get('dompdf.defines');
|
||||
|
||||
if ($defines) {
|
||||
$options = [];
|
||||
/**
|
||||
* @var string $key
|
||||
* @var mixed $value
|
||||
*/
|
||||
foreach ($defines as $key => $value) {
|
||||
$key = strtolower(str_replace('DOMPDF_', '', $key));
|
||||
$options[$key] = $value;
|
||||
}
|
||||
} else {
|
||||
$options = $app['config']->get('dompdf.options');
|
||||
}
|
||||
|
||||
return $options;
|
||||
});
|
||||
|
||||
$this->app->bind('dompdf', function ($app) {
|
||||
|
||||
$options = $app->make('dompdf.options');
|
||||
$dompdf = new Dompdf($options);
|
||||
$path = realpath($app['config']->get('dompdf.public_path') ?: base_path('public'));
|
||||
if ($path === false) {
|
||||
throw new \RuntimeException('Cannot resolve public path');
|
||||
}
|
||||
$dompdf->setBasePath($path);
|
||||
|
||||
return $dompdf;
|
||||
});
|
||||
$this->app->alias('dompdf', Dompdf::class);
|
||||
|
||||
$this->app->bind('dompdf.wrapper', function ($app) {
|
||||
return new PDF($app['dompdf'], $app['config'], $app['files'], $app['view']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if package is running under Lumen app
|
||||
*/
|
||||
protected function isLumen(): bool
|
||||
{
|
||||
return Str::contains($this->app->version(), 'Lumen') === true;
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
if (! $this->isLumen()) {
|
||||
$configPath = __DIR__ . '/../config/dompdf.php';
|
||||
$this->publishes([$configPath => config_path('dompdf.php')], 'config');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return ['dompdf', 'dompdf.options', 'dompdf.wrapper'];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/carbon
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/patch-type-declarations
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/php-parse
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/phpunit
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../laravel/pint/builds/pint)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/laravel/pint/builds/pint');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/laravel/pint/builds/pint';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/pint
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/psysh
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Support bash to support `source` with fallback on $0 if this does not run with bash
|
||||
# https://stackoverflow.com/a/35006505/6512
|
||||
selfArg="$BASH_SOURCE"
|
||||
if [ -z "$selfArg" ]; then
|
||||
selfArg="$0"
|
||||
fi
|
||||
|
||||
self=$(realpath $selfArg 2> /dev/null)
|
||||
if [ -z "$self" ]; then
|
||||
self="$selfArg"
|
||||
fi
|
||||
|
||||
dir=$(cd "${self%[/\\]*}" > /dev/null; cd ../laravel/sail/bin && pwd)
|
||||
|
||||
if [ -d /proc/cygdrive ]; then
|
||||
case $(which php) in
|
||||
$(readlink -n /proc/cygdrive)/*)
|
||||
# We are in Cygwin using Windows php, so the path must be translated
|
||||
dir=$(cygpath -m "$dir");
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
|
||||
|
||||
# If bash is sourcing this file, we have to source the target as well
|
||||
bashSource="$BASH_SOURCE"
|
||||
if [ -n "$bashSource" ]; then
|
||||
if [ "$bashSource" != "$0" ]; then
|
||||
source "${dir}/sail" "$@"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
"${dir}/sail" "$@"
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/../laravel/sail/bin/sail
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
bash "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/var-dump-server
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';
|
|
@ -0,0 +1,5 @@
|
|||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/yaml-lint
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
|
@ -0,0 +1,479 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigDecimal::getPrecision()` Returns the number of significant digits in a decimal number
|
||||
|
||||
## [0.12.2](https://github.com/brick/math/releases/tag/0.12.2) - 2025-02-26
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- Division in `NativeCalculator` is now faster for small divisors, thanks to [@Izumi-kun](https://github.com/Izumi-kun) in [#87](https://github.com/brick/math/pull/87).
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Add missing `RoundingNecessaryException` to the `@throws` annotation of `BigNumber::of()`
|
||||
|
||||
## [0.12.1](https://github.com/brick/math/releases/tag/0.12.1) - 2023-11-29
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- `BigNumber::of()` is now faster, thanks to [@SebastienDug](https://github.com/SebastienDug) in [#77](https://github.com/brick/math/pull/77).
|
||||
|
||||
## [0.12.0](https://github.com/brick/math/releases/tag/0.12.0) - 2023-11-26
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.1
|
||||
- `RoundingMode` is now an `enum`; if you're type-hinting rounding modes, you need to type-hint against `RoundingMode` instead of `int` now
|
||||
- `BigNumber` classes do not implement the `Serializable` interface anymore (they use the [new custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization))
|
||||
- The following breaking changes only affect you if you're creating your own `BigNumber` subclasses:
|
||||
- the return type of `BigNumber::of()` is now `static`
|
||||
- `BigNumber` has a new abstract method `from()`
|
||||
- all `public` and `protected` functions of `BigNumber` are now `final`
|
||||
|
||||
## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.0
|
||||
- Methods accepting a union of types are now strongly typed<sup>*</sup>
|
||||
- `MathException` now extends `Exception` instead of `RuntimeException`
|
||||
|
||||
<sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods
|
||||
internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string`
|
||||
first.</sup>
|
||||
|
||||
## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic
|
||||
|
||||
## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers
|
||||
|
||||
## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 7.4
|
||||
|
||||
## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
|
||||
|
||||
🚀 **Compatibility with PHP 8.1**
|
||||
|
||||
- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (#60) thanks @TRowbotham
|
||||
|
||||
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
|
||||
|
||||
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::not()` returns the bitwise `NOT` value
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
|
||||
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
|
||||
|
||||
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
|
||||
|
||||
🚑 **Critical fix**
|
||||
|
||||
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
|
||||
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
|
||||
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
|
||||
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
|
||||
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
|
||||
|
||||
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
|
||||
|
||||
🐛 **Fixes**
|
||||
|
||||
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
|
||||
|
||||
⚡️ **Optimizations**
|
||||
|
||||
- additional optimization in `BigInteger::remainder()`
|
||||
|
||||
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
|
||||
|
||||
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::isEven()` tests whether the number is even
|
||||
- `BigInteger::isOdd()` tests whether the number is odd
|
||||
- `BigInteger::testBit()` tests if a bit is set
|
||||
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
|
||||
|
||||
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
|
||||
|
||||
🛠️ **Maintenance release**
|
||||
|
||||
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
|
||||
|
||||
This is a maintenance release: no bug fixes, no new features, no breaking changes.
|
||||
|
||||
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
|
||||
|
||||
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
|
||||
|
||||
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
|
||||
|
||||
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
|
||||
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
|
||||
|
||||
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
|
||||
|
||||
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
|
||||
|
||||
- the `$base` parameter is required, it does not default to `10`
|
||||
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
|
||||
|
||||
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
|
||||
|
||||
**Improvements**
|
||||
|
||||
- Safer conversion from `float` when using custom locales
|
||||
- **Much faster** `NativeCalculator` implementation 🚀
|
||||
|
||||
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
|
||||
|
||||
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
|
||||
|
||||
**New method**
|
||||
|
||||
`BigNumber::sum()` returns the sum of one or more numbers.
|
||||
|
||||
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
|
||||
|
||||
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
|
||||
|
||||
Thanks @manowark 👍
|
||||
|
||||
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
|
||||
|
||||
**New method**
|
||||
|
||||
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
|
||||
|
||||
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
|
||||
|
||||
**New method**
|
||||
|
||||
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
|
||||
|
||||
**New exception**
|
||||
|
||||
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
|
||||
|
||||
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
|
||||
|
||||
**Performance update**
|
||||
|
||||
- Further improvement of `toInt()` performance
|
||||
- `NativeCalculator` can now perform some multiplications more efficiently
|
||||
|
||||
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
|
||||
|
||||
Performance optimization of `toInt()` methods.
|
||||
|
||||
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
The following deprecated methods have been removed. Use the new method name instead:
|
||||
|
||||
| Method removed | Replacement method |
|
||||
| --- | --- |
|
||||
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
|
||||
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
|
||||
|
||||
---
|
||||
|
||||
**New features**
|
||||
|
||||
`BigInteger` has been augmented with 5 new methods for bitwise operations:
|
||||
|
||||
| New method | Description |
|
||||
| --- | --- |
|
||||
| `and()` | performs a bitwise `AND` operation on two numbers |
|
||||
| `or()` | performs a bitwise `OR` operation on two numbers |
|
||||
| `xor()` | performs a bitwise `XOR` operation on two numbers |
|
||||
| `shiftedLeft()` | returns the number shifted left by a number of bits |
|
||||
| `shiftedRight()` | returns the number shifted right by a number of bits |
|
||||
|
||||
Thanks to @DASPRiD 👍
|
||||
|
||||
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
|
||||
|
||||
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
|
||||
|
||||
**Renamed/deprecated methods:**
|
||||
|
||||
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
|
||||
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
|
||||
|
||||
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
|
||||
|
||||
**Performance update**
|
||||
|
||||
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
|
||||
|
||||
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
|
||||
|
||||
This is a maintenance release, no code has been changed.
|
||||
|
||||
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
|
||||
- Tests and other files unnecessary for production are excluded from the dist package
|
||||
|
||||
This will help make installations more compact.
|
||||
|
||||
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
|
||||
|
||||
Methods renamed:
|
||||
|
||||
- `BigNumber:sign()` has been renamed to `getSign()`
|
||||
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
|
||||
- `BigDecimal::scale()` has been renamed to `getScale()`
|
||||
- `BigDecimal::integral()` has been renamed to `getIntegral()`
|
||||
- `BigDecimal::fraction()` has been renamed to `getFraction()`
|
||||
- `BigRational::numerator()` has been renamed to `getNumerator()`
|
||||
- `BigRational::denominator()` has been renamed to `getDenominator()`
|
||||
|
||||
Classes renamed:
|
||||
|
||||
- `ArithmeticException` has been renamed to `MathException`
|
||||
|
||||
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
|
||||
|
||||
The base class for all exceptions is now `MathException`.
|
||||
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
|
||||
|
||||
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
|
||||
|
||||
A number of methods have been renamed:
|
||||
|
||||
- `BigNumber:sign()` is deprecated; use `getSign()` instead
|
||||
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
|
||||
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
|
||||
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
|
||||
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
|
||||
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
|
||||
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
|
||||
|
||||
The old methods will be removed in version 0.7.0.
|
||||
|
||||
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
|
||||
|
||||
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
|
||||
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
|
||||
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
|
||||
|
||||
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
|
||||
|
||||
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
|
||||
The JSON output is always a string.
|
||||
|
||||
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
|
||||
|
||||
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
|
||||
|
||||
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
|
||||
|
||||
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
|
||||
|
||||
**New method: `BigNumber::toScale()`**
|
||||
|
||||
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
|
||||
|
||||
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
|
||||
|
||||
**New features**
|
||||
- Common `BigNumber` interface for all classes, with the following methods:
|
||||
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
|
||||
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
|
||||
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
|
||||
- `toInteger()` and `toFloat()` conversion methods to native types
|
||||
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
|
||||
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
|
||||
- New methods: `BigRational::quotient()` and `remainder()`
|
||||
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
|
||||
- Factory methods `zero()`, `one()` and `ten()` available in all classes
|
||||
- Rounding mode reintroduced in `BigInteger::dividedBy()`
|
||||
|
||||
This release also comes with many performance improvements.
|
||||
|
||||
---
|
||||
|
||||
**Breaking changes**
|
||||
- `BigInteger`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `toString()` is renamed to `toBase()`
|
||||
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
|
||||
- `BigDecimal`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getUnscaledValue()` is renamed to `unscaledValue()`
|
||||
- `getScale()` is renamed to `scale()`
|
||||
- `getIntegral()` is renamed to `integral()`
|
||||
- `getFraction()` is renamed to `fraction()`
|
||||
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
|
||||
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
|
||||
- `toBigInteger()` does not accept a `$roundingMode` parameter anymore
|
||||
- `toBigRational()` does not simplify the fraction anymore; explicitly add `->simplified()` to get the previous behaviour
|
||||
- `BigRational`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getNumerator()` is renamed to `numerator()`
|
||||
- `getDenominator()` is renamed to `denominator()`
|
||||
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
|
||||
- Miscellaneous:
|
||||
- `ArithmeticException` is moved to an `Exception\` sub-namespace
|
||||
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
|
||||
|
||||
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
|
||||
|
||||
New method: `BigDecimal::stripTrailingZeros()`
|
||||
|
||||
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
|
||||
|
||||
Introducing a `BigRational` class, to perform calculations on fractions of any size.
|
||||
|
||||
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
|
||||
|
||||
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
|
||||
|
||||
`BigInteger::dividedBy()` now always returns the quotient of the division.
|
||||
|
||||
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
|
||||
|
||||
New methods:
|
||||
- `BigInteger::remainder()` returns the remainder of a division only
|
||||
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
|
||||
|
||||
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
|
||||
|
||||
Fix `toString()` not handling negative numbers.
|
||||
|
||||
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
|
||||
|
||||
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
|
||||
- `-1` if the number is negative
|
||||
- `0` if the number is zero
|
||||
- `1` if the number is positive
|
||||
|
||||
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
|
||||
|
||||
Minor performance improvements
|
||||
|
||||
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
|
||||
|
||||
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
|
||||
|
||||
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
|
||||
|
||||
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
|
||||
|
||||
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
|
||||
|
||||
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
|
||||
|
||||
Added `BigDecimal::divideAndRemainder()`
|
||||
|
||||
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
|
||||
|
||||
- `min()` and `max()` do not accept an `array` anymore, but a variable number of parameters
|
||||
- **minimum PHP version is now 5.6**
|
||||
- continuous integration with PHP 7
|
||||
|
||||
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
|
||||
|
||||
- Added `BigInteger::power()`
|
||||
- Added HHVM support
|
||||
|
||||
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
|
||||
|
||||
First beta release.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-present Benjamin Morel
|
||||
|
||||
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,39 @@
|
|||
{
|
||||
"name": "brick/math",
|
||||
"description": "Arbitrary-precision arithmetic library",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"Brick",
|
||||
"Math",
|
||||
"Mathematics",
|
||||
"Arbitrary-precision",
|
||||
"Arithmetic",
|
||||
"BigInteger",
|
||||
"BigDecimal",
|
||||
"BigRational",
|
||||
"BigNumber",
|
||||
"Bignum",
|
||||
"Decimal",
|
||||
"Rational",
|
||||
"Integer"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"php-coveralls/php-coveralls": "^2.2",
|
||||
"vimeo/psalm": "6.8.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="6.8.8@1361cd33008feb3ae2b4a93f1860e14e538ec8c2">
|
||||
<file src="src/BigInteger.php">
|
||||
<FalsableReturnStatement>
|
||||
<code><![CDATA[\hex2bin($hex)]]></code>
|
||||
</FalsableReturnStatement>
|
||||
<InvalidFalsableReturnType>
|
||||
<code><![CDATA[string]]></code>
|
||||
</InvalidFalsableReturnType>
|
||||
</file>
|
||||
<file src="src/Exception/DivisionByZeroException.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[DivisionByZeroException]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Exception/IntegerOverflowException.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[IntegerOverflowException]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Exception/NegativeNumberException.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[NegativeNumberException]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Exception/NumberFormatException.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[NumberFormatException]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Exception/RoundingNecessaryException.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[RoundingNecessaryException]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Internal/Calculator/BcMathCalculator.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[BcMathCalculator]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Internal/Calculator/GmpCalculator.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[GmpCalculator]]></code>
|
||||
</ClassMustBeFinal>
|
||||
</file>
|
||||
<file src="src/Internal/Calculator/NativeCalculator.php">
|
||||
<ClassMustBeFinal>
|
||||
<code><![CDATA[NativeCalculator]]></code>
|
||||
</ClassMustBeFinal>
|
||||
<InvalidOperand>
|
||||
<code><![CDATA[$a * $b]]></code>
|
||||
<code><![CDATA[$a * 1]]></code>
|
||||
<code><![CDATA[$a + $b]]></code>
|
||||
<code><![CDATA[$b * 1]]></code>
|
||||
<code><![CDATA[$b * 1]]></code>
|
||||
<code><![CDATA[$blockA * $blockB + $carry]]></code>
|
||||
<code><![CDATA[$blockA + $blockB]]></code>
|
||||
<code><![CDATA[$blockA + $blockB + $carry]]></code>
|
||||
<code><![CDATA[$blockA - $blockB]]></code>
|
||||
<code><![CDATA[$blockA - $blockB - $carry]]></code>
|
||||
<code><![CDATA[$carry]]></code>
|
||||
<code><![CDATA[$mul % $complement]]></code>
|
||||
<code><![CDATA[$mul - $value]]></code>
|
||||
<code><![CDATA[$nb - 1]]></code>
|
||||
<code><![CDATA[$sum += $complement]]></code>
|
||||
<code><![CDATA[($mul - $value) / $complement]]></code>
|
||||
<code><![CDATA[($nb - 1) * 10]]></code>
|
||||
</InvalidOperand>
|
||||
</file>
|
||||
</files>
|
|
@ -0,0 +1,792 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NegativeNumberException;
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Immutable, arbitrary-precision signed decimal numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigDecimal extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The unscaled value of this decimal number.
|
||||
*
|
||||
* This is a string of digits with an optional leading minus sign.
|
||||
* No leading zero must be present.
|
||||
* No leading minus sign must be present if the value is 0.
|
||||
*/
|
||||
private readonly string $value;
|
||||
|
||||
/**
|
||||
* The scale (number of digits after the decimal point) of this decimal number.
|
||||
*
|
||||
* This must be zero or more.
|
||||
*/
|
||||
private readonly int $scale;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param string $value The unscaled value, validated.
|
||||
* @param int $scale The scale, validated.
|
||||
*/
|
||||
protected function __construct(string $value, int $scale = 0)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->scale = $scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigDecimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigDecimal from an unscaled value and a scale.
|
||||
*
|
||||
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
|
||||
* @param int $scale The scale of the number, positive or zero.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale is negative.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
|
||||
{
|
||||
if ($scale < 0) {
|
||||
throw new \InvalidArgumentException('The scale cannot be negative.');
|
||||
}
|
||||
|
||||
return new BigDecimal((string) BigInteger::of($value), $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing zero, with a scale of zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function zero() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $zero
|
||||
*/
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigDecimal('0');
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing one, with a scale of zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function one() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $one
|
||||
*/
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigDecimal('1');
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing ten, with a scale of zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ten() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $ten
|
||||
*/
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigDecimal('10');
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function plus(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->value === '0' && $this->scale <= $that->scale) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$value = Calculator::get()->add($a, $b);
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function minus(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$value = Calculator::get()->sub($a, $b);
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `$this->scale + $that->scale`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '1' && $that->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->value === '1' && $this->scale === 0) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
$value = Calculator::get()->mul($this->value, $that->value);
|
||||
$scale = $this->scale + $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one, at the given scale.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor.
|
||||
* @param int|null $scale The desired scale, or null to use the scale of this number.
|
||||
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
|
||||
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
|
||||
*/
|
||||
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
if ($scale === null) {
|
||||
$scale = $this->scale;
|
||||
} elseif ($scale < 0) {
|
||||
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||
}
|
||||
|
||||
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale + $scale);
|
||||
$q = $that->valueWithMinScale($this->scale - $scale);
|
||||
|
||||
$result = Calculator::get()->divRound($p, $q, $roundingMode);
|
||||
|
||||
return new BigDecimal($result, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exact result of the division of this number by the given one.
|
||||
*
|
||||
* The scale of the result is automatically calculated to fit all the fraction digits.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
|
||||
* or the result yields an infinite number of digits.
|
||||
*/
|
||||
public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0') {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
[, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$d = \rtrim($b, '0');
|
||||
$scale = \strlen($b) - \strlen($d);
|
||||
|
||||
$calculator = Calculator::get();
|
||||
|
||||
foreach ([5, 2] as $prime) {
|
||||
for (;;) {
|
||||
$lastDigit = (int) $d[-1];
|
||||
|
||||
if ($lastDigit % $prime !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$d = $calculator->divQ($d, (string) $prime);
|
||||
$scale++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dividedBy($that, $scale)->stripTrailingZeros();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* The result has a scale of `$this->scale * $exponent`.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*/
|
||||
public function power(int $exponent) : BigDecimal
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
return BigDecimal::one();
|
||||
}
|
||||
|
||||
if ($exponent === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'The exponent %d is not in the range 0 to %d.',
|
||||
$exponent,
|
||||
Calculator::MAX_POWER
|
||||
));
|
||||
}
|
||||
|
||||
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of this number by the given one.
|
||||
*
|
||||
* The quotient has a scale of `0`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function quotient(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$quotient = Calculator::get()->divQ($p, $q);
|
||||
|
||||
return new BigDecimal($quotient, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of this number by the given one.
|
||||
*
|
||||
* The remainder has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function remainder(BigNumber|int|float|string $that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$remainder = Calculator::get()->divR($p, $q);
|
||||
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($remainder, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of this number by the given one.
|
||||
*
|
||||
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal[] An array containing the quotient and the remainder.
|
||||
*
|
||||
* @psalm-return array{BigDecimal, BigDecimal}
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function quotientAndRemainder(BigNumber|int|float|string $that) : array
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
[$quotient, $remainder] = Calculator::get()->divQR($p, $q);
|
||||
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
$quotient = new BigDecimal($quotient, 0);
|
||||
$remainder = new BigDecimal($remainder, $scale);
|
||||
|
||||
return [$quotient, $remainder];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square root of this number, rounded down to the given number of decimals.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale is negative.
|
||||
* @throws NegativeNumberException If this number is negative.
|
||||
*/
|
||||
public function sqrt(int $scale) : BigDecimal
|
||||
{
|
||||
if ($scale < 0) {
|
||||
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||
}
|
||||
|
||||
if ($this->value === '0') {
|
||||
return new BigDecimal('0', $scale);
|
||||
}
|
||||
|
||||
if ($this->value[0] === '-') {
|
||||
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
|
||||
}
|
||||
|
||||
$value = $this->value;
|
||||
$addDigits = 2 * $scale - $this->scale;
|
||||
|
||||
if ($addDigits > 0) {
|
||||
// add zeros
|
||||
$value .= \str_repeat('0', $addDigits);
|
||||
} elseif ($addDigits < 0) {
|
||||
// trim digits
|
||||
if (-$addDigits >= \strlen($this->value)) {
|
||||
// requesting a scale too low, will always yield a zero result
|
||||
return new BigDecimal('0', $scale);
|
||||
}
|
||||
|
||||
$value = \substr($value, 0, $addDigits);
|
||||
}
|
||||
|
||||
$value = Calculator::get()->sqrt($value);
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
|
||||
*/
|
||||
public function withPointMovedLeft(int $n) : BigDecimal
|
||||
{
|
||||
if ($n === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($n < 0) {
|
||||
return $this->withPointMovedRight(-$n);
|
||||
}
|
||||
|
||||
return new BigDecimal($this->value, $this->scale + $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
|
||||
*/
|
||||
public function withPointMovedRight(int $n) : BigDecimal
|
||||
{
|
||||
if ($n === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($n < 0) {
|
||||
return $this->withPointMovedLeft(-$n);
|
||||
}
|
||||
|
||||
$value = $this->value;
|
||||
$scale = $this->scale - $n;
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($value !== '0') {
|
||||
$value .= \str_repeat('0', -$scale);
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
|
||||
*/
|
||||
public function stripTrailingZeros() : BigDecimal
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$trimmedValue = \rtrim($this->value, '0');
|
||||
|
||||
if ($trimmedValue === '') {
|
||||
return BigDecimal::zero();
|
||||
}
|
||||
|
||||
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
|
||||
|
||||
if ($trimmableZeros === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($trimmableZeros > $this->scale) {
|
||||
$trimmableZeros = $this->scale;
|
||||
}
|
||||
|
||||
$value = \substr($this->value, 0, -$trimmableZeros);
|
||||
$scale = $this->scale - $trimmableZeros;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this number.
|
||||
*/
|
||||
public function abs() : BigDecimal
|
||||
{
|
||||
return $this->isNegative() ? $this->negated() : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this number.
|
||||
*/
|
||||
public function negated() : BigDecimal
|
||||
{
|
||||
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|float|string $that) : int
|
||||
{
|
||||
$that = BigNumber::of($that);
|
||||
|
||||
if ($that instanceof BigInteger) {
|
||||
$that = $that->toBigDecimal();
|
||||
}
|
||||
|
||||
if ($that instanceof BigDecimal) {
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
return Calculator::get()->cmp($a, $b);
|
||||
}
|
||||
|
||||
return - $that->compareTo($this);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign() : int
|
||||
{
|
||||
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
|
||||
}
|
||||
|
||||
public function getUnscaledValue() : BigInteger
|
||||
{
|
||||
return self::newBigInteger($this->value);
|
||||
}
|
||||
|
||||
public function getScale() : int
|
||||
{
|
||||
return $this->scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of significant digits in the number.
|
||||
*
|
||||
* This is the number of digits to both sides of the decimal point, stripped of leading zeros.
|
||||
* The sign has no impact on the result.
|
||||
*
|
||||
* Examples:
|
||||
* 0 => 0
|
||||
* 0.0 => 0
|
||||
* 123 => 3
|
||||
* 123.456 => 6
|
||||
* 0.00123 => 3
|
||||
* 0.0012300 => 5
|
||||
*/
|
||||
public function getPrecision(): int
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
if ($value === '0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$length = \strlen($value);
|
||||
|
||||
return ($value[0] === '-') ? $length - 1 : $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the integral part of this decimal number.
|
||||
*
|
||||
* Example: `-123.456` => `-123`.
|
||||
*/
|
||||
public function getIntegralPart() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, 0, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the fractional part of this decimal number.
|
||||
*
|
||||
* If the scale is zero, an empty string is returned.
|
||||
*
|
||||
* Examples: `-123.456` => '456', `123` => ''.
|
||||
*/
|
||||
public function getFractionalPart() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this decimal number has a non-zero fractional part.
|
||||
*/
|
||||
public function hasNonZeroFractionalPart() : bool
|
||||
{
|
||||
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger() : BigInteger
|
||||
{
|
||||
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
|
||||
|
||||
return self::newBigInteger($zeroScaleDecimal->value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal() : BigDecimal
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigRational() : BigRational
|
||||
{
|
||||
$numerator = self::newBigInteger($this->value);
|
||||
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
|
||||
|
||||
return self::newBigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
if ($scale === $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toInt() : int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toFloat() : float
|
||||
{
|
||||
return (float) (string) $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function __toString() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return array{value: string, scale: int}
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return ['value' => $this->value, 'scale' => $this->scale];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only here to allow unserializing the object and cannot be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||
*
|
||||
* @param array{value: string, scale: int} $data
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
if (isset($this->value)) {
|
||||
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
$this->value = $data['value'];
|
||||
$this->scale = $data['scale'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the internal values of the given decimal numbers on the same scale.
|
||||
*
|
||||
* @return array{string, string} The scaled integer values of $x and $y.
|
||||
*/
|
||||
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
|
||||
{
|
||||
$a = $x->value;
|
||||
$b = $y->value;
|
||||
|
||||
if ($b !== '0' && $x->scale > $y->scale) {
|
||||
$b .= \str_repeat('0', $x->scale - $y->scale);
|
||||
} elseif ($a !== '0' && $x->scale < $y->scale) {
|
||||
$a .= \str_repeat('0', $y->scale - $x->scale);
|
||||
}
|
||||
|
||||
return [$a, $b];
|
||||
}
|
||||
|
||||
private function valueWithMinScale(int $scale) : string
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
if ($this->value !== '0' && $scale > $this->scale) {
|
||||
$value .= \str_repeat('0', $scale - $this->scale);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
|
||||
*/
|
||||
private function getUnscaledValueWithLeadingZeros() : string
|
||||
{
|
||||
$value = $this->value;
|
||||
$targetLength = $this->scale + 1;
|
||||
$negative = ($value[0] === '-');
|
||||
$length = \strlen($value);
|
||||
|
||||
if ($negative) {
|
||||
$length--;
|
||||
}
|
||||
|
||||
if ($length >= $targetLength) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = \substr($value, 1);
|
||||
}
|
||||
|
||||
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,515 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Common interface for arbitrary-precision rational numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class BigNumber implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The regular expression used to parse integer or decimal numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_NUMERICAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<integral>[0-9]+)?' .
|
||||
'(?<point>\.)?' .
|
||||
'(?<fractional>[0-9]+)?' .
|
||||
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse rational numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_RATIONAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<numerator>[0-9]+)' .
|
||||
'\/?' .
|
||||
'(?<denominator>[0-9]+)' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value.
|
||||
*
|
||||
* The concrete return type is dependent on the given value, with the following rules:
|
||||
*
|
||||
* - BigNumber instances are returned as is
|
||||
* - integer numbers are returned as BigInteger
|
||||
* - floating point numbers are converted to a string then parsed as such
|
||||
* - strings containing a `/` character are returned as BigRational
|
||||
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
final public static function of(BigNumber|int|float|string $value) : static
|
||||
{
|
||||
$value = self::_of($value);
|
||||
|
||||
if (static::class === BigNumber::class) {
|
||||
// https://github.com/vimeo/psalm/issues/10309
|
||||
assert($value instanceof static);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
return static::from($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function _of(BigNumber|int|float|string $value) : BigNumber
|
||||
{
|
||||
if ($value instanceof BigNumber) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (\is_int($value)) {
|
||||
return new BigInteger((string) $value);
|
||||
}
|
||||
|
||||
if (is_float($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
|
||||
if (str_contains($value, '/')) {
|
||||
// Rational number
|
||||
if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$numerator = $matches['numerator'];
|
||||
$denominator = $matches['denominator'];
|
||||
|
||||
assert($numerator !== null);
|
||||
assert($denominator !== null);
|
||||
|
||||
$numerator = self::cleanUp($sign, $numerator);
|
||||
$denominator = self::cleanUp(null, $denominator);
|
||||
|
||||
if ($denominator === '0') {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
new BigInteger($numerator),
|
||||
new BigInteger($denominator),
|
||||
false
|
||||
);
|
||||
} else {
|
||||
// Integer or decimal number
|
||||
if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$point = $matches['point'];
|
||||
$integral = $matches['integral'];
|
||||
$fractional = $matches['fractional'];
|
||||
$exponent = $matches['exponent'];
|
||||
|
||||
if ($integral === null && $fractional === null) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
if ($integral === null) {
|
||||
$integral = '0';
|
||||
}
|
||||
|
||||
if ($point !== null || $exponent !== null) {
|
||||
$fractional = ($fractional ?? '');
|
||||
$exponent = ($exponent !== null) ? (int)$exponent : 0;
|
||||
|
||||
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
|
||||
throw new NumberFormatException('Exponent too large.');
|
||||
}
|
||||
|
||||
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
|
||||
|
||||
$scale = \strlen($fractional) - $exponent;
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($unscaledValue !== '0') {
|
||||
$unscaledValue .= \str_repeat('0', -$scale);
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($unscaledValue, $scale);
|
||||
}
|
||||
|
||||
$integral = self::cleanUp($sign, $integral);
|
||||
|
||||
return new BigInteger($integral);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
|
||||
*
|
||||
* @throws RoundingNecessaryException If the value cannot be converted.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
abstract protected static function from(BigNumber $number): static;
|
||||
|
||||
/**
|
||||
* Proxy method to access BigInteger's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-pure
|
||||
*/
|
||||
final protected function newBigInteger(string $value) : BigInteger
|
||||
{
|
||||
return new BigInteger($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigDecimal's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-pure
|
||||
*/
|
||||
final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
|
||||
{
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigRational's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-pure
|
||||
*/
|
||||
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
|
||||
{
|
||||
return new BigRational($numerator, $denominator, $checkDenominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
final public static function min(BigNumber|int|float|string ...$values) : static
|
||||
{
|
||||
$min = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($min === null || $value->isLessThan($min)) {
|
||||
$min = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($min === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
final public static function max(BigNumber|int|float|string ...$values) : static
|
||||
{
|
||||
$max = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($max === null || $value->isGreaterThan($max)) {
|
||||
$max = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($max === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
final public static function sum(BigNumber|int|float|string ...$values) : static
|
||||
{
|
||||
/** @var static|null $sum */
|
||||
$sum = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
$sum = $sum === null ? $value : self::add($sum, $value);
|
||||
}
|
||||
|
||||
if ($sum === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
|
||||
*
|
||||
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
|
||||
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
|
||||
* depending on their ability to perform the operation. This will also require a version bump because we're
|
||||
* potentially breaking custom BigNumber implementations (if any...)
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function add(BigNumber $a, BigNumber $b) : BigNumber
|
||||
{
|
||||
if ($a instanceof BigRational) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigRational) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
if ($a instanceof BigDecimal) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigDecimal) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
/** @var BigInteger $a */
|
||||
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes optional leading zeros and applies sign.
|
||||
*
|
||||
* @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
|
||||
* @param string $number The number, validated as a non-empty string of digits.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function cleanUp(string|null $sign, string $number) : string
|
||||
{
|
||||
$number = \ltrim($number, '0');
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $sign === '-' ? '-' . $number : $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is equal to the given one.
|
||||
*/
|
||||
final public function isEqualTo(BigNumber|int|float|string $that) : bool
|
||||
{
|
||||
return $this->compareTo($that) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly lower than the given one.
|
||||
*/
|
||||
final public function isLessThan(BigNumber|int|float|string $that) : bool
|
||||
{
|
||||
return $this->compareTo($that) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is lower than or equal to the given one.
|
||||
*/
|
||||
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
|
||||
{
|
||||
return $this->compareTo($that) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly greater than the given one.
|
||||
*/
|
||||
final public function isGreaterThan(BigNumber|int|float|string $that) : bool
|
||||
{
|
||||
return $this->compareTo($that) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is greater than or equal to the given one.
|
||||
*/
|
||||
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
|
||||
{
|
||||
return $this->compareTo($that) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number equals zero.
|
||||
*/
|
||||
final public function isZero() : bool
|
||||
{
|
||||
return $this->getSign() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly negative.
|
||||
*/
|
||||
final public function isNegative() : bool
|
||||
{
|
||||
return $this->getSign() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is negative or zero.
|
||||
*/
|
||||
final public function isNegativeOrZero() : bool
|
||||
{
|
||||
return $this->getSign() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly positive.
|
||||
*/
|
||||
final public function isPositive() : bool
|
||||
{
|
||||
return $this->getSign() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is positive or zero.
|
||||
*/
|
||||
final public function isPositiveOrZero() : bool
|
||||
{
|
||||
return $this->getSign() >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sign of this number.
|
||||
*
|
||||
* @psalm-return -1|0|1
|
||||
*
|
||||
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
|
||||
*/
|
||||
abstract public function getSign() : int;
|
||||
|
||||
/**
|
||||
* Compares this number to the given one.
|
||||
*
|
||||
* @psalm-return -1|0|1
|
||||
*
|
||||
* @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
abstract public function compareTo(BigNumber|int|float|string $that) : int;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigInteger.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
|
||||
*/
|
||||
abstract public function toBigInteger() : BigInteger;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
|
||||
*/
|
||||
abstract public function toBigDecimal() : BigDecimal;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigRational.
|
||||
*/
|
||||
abstract public function toBigRational() : BigRational;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
|
||||
*
|
||||
* @param int $scale The scale of the resulting `BigDecimal`.
|
||||
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
|
||||
* This only applies when RoundingMode::UNNECESSARY is used.
|
||||
*/
|
||||
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
|
||||
|
||||
/**
|
||||
* Returns the exact value of this number as a native integer.
|
||||
*
|
||||
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
|
||||
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
|
||||
*
|
||||
* @throws MathException If this number cannot be exactly converted to a native integer.
|
||||
*/
|
||||
abstract public function toInt() : int;
|
||||
|
||||
/**
|
||||
* Returns an approximation of this number as a floating-point value.
|
||||
*
|
||||
* Note that this method can discard information as the precision of a floating-point value
|
||||
* is inherently limited.
|
||||
*
|
||||
* If the number is greater than the largest representable floating point number, positive infinity is returned.
|
||||
* If the number is less than the smallest representable floating point number, negative infinity is returned.
|
||||
*/
|
||||
abstract public function toFloat() : float;
|
||||
|
||||
/**
|
||||
* Returns a string representation of this number.
|
||||
*
|
||||
* The output of this method can be parsed by the `of()` factory method;
|
||||
* this will yield an object equal to this one, without any information loss.
|
||||
*/
|
||||
abstract public function __toString() : string;
|
||||
|
||||
#[Override]
|
||||
final public function jsonSerialize() : string
|
||||
{
|
||||
return $this->__toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,424 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* An arbitrarily large rational number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigRational extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The numerator.
|
||||
*/
|
||||
private readonly BigInteger $numerator;
|
||||
|
||||
/**
|
||||
* The denominator. Always strictly positive.
|
||||
*/
|
||||
private readonly BigInteger $denominator;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param BigInteger $numerator The numerator.
|
||||
* @param BigInteger $denominator The denominator.
|
||||
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||
*
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*/
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
|
||||
{
|
||||
if ($checkDenominator) {
|
||||
if ($denominator->isZero()) {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
if ($denominator->isNegative()) {
|
||||
$numerator = $numerator->negated();
|
||||
$denominator = $denominator->negated();
|
||||
}
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigRational();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigRational out of a numerator and a denominator.
|
||||
*
|
||||
* If the denominator is negative, the signs of both the numerator and the denominator
|
||||
* will be inverted to ensure that the denominator is always positive.
|
||||
*
|
||||
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
|
||||
*
|
||||
* @throws NumberFormatException If an argument does not represent a valid number.
|
||||
* @throws RoundingNecessaryException If an argument represents a non-integer number.
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function nd(
|
||||
BigNumber|int|float|string $numerator,
|
||||
BigNumber|int|float|string $denominator,
|
||||
) : BigRational {
|
||||
$numerator = BigInteger::of($numerator);
|
||||
$denominator = BigInteger::of($denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function zero() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $zero
|
||||
*/
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing one.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function one() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $one
|
||||
*/
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing ten.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ten() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $ten
|
||||
*/
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
public function getNumerator() : BigInteger
|
||||
{
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
public function getDenominator() : BigInteger
|
||||
{
|
||||
return $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of the numerator by the denominator.
|
||||
*/
|
||||
public function quotient() : BigInteger
|
||||
{
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of the numerator by the denominator.
|
||||
*/
|
||||
public function remainder() : BigInteger
|
||||
{
|
||||
return $this->numerator->remainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of the numerator by the denominator.
|
||||
*
|
||||
* @return BigInteger[]
|
||||
*
|
||||
* @psalm-return array{BigInteger, BigInteger}
|
||||
*/
|
||||
public function quotientAndRemainder() : array
|
||||
{
|
||||
return $this->numerator->quotientAndRemainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
public function plus(BigNumber|int|float|string $that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to subtract.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
public function minus(BigNumber|int|float|string $that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The multiplier.
|
||||
*
|
||||
* @throws MathException If the multiplier is not a valid number.
|
||||
*/
|
||||
public function multipliedBy(BigNumber|int|float|string $that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid number, or is zero.
|
||||
*/
|
||||
public function dividedBy(BigNumber|int|float|string $that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$denominator = $this->denominator->multipliedBy($that->numerator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*/
|
||||
public function power(int $exponent) : BigRational
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
$one = BigInteger::one();
|
||||
|
||||
return new BigRational($one, $one, false);
|
||||
}
|
||||
|
||||
if ($exponent === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
$this->numerator->power($exponent),
|
||||
$this->denominator->power($exponent),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reciprocal of this BigRational.
|
||||
*
|
||||
* The reciprocal has the numerator and denominator swapped.
|
||||
*
|
||||
* @throws DivisionByZeroException If the numerator is zero.
|
||||
*/
|
||||
public function reciprocal() : BigRational
|
||||
{
|
||||
return new BigRational($this->denominator, $this->numerator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this BigRational.
|
||||
*/
|
||||
public function abs() : BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->abs(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this BigRational.
|
||||
*/
|
||||
public function negated() : BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simplified value of this BigRational.
|
||||
*/
|
||||
public function simplified() : BigRational
|
||||
{
|
||||
$gcd = $this->numerator->gcd($this->denominator);
|
||||
|
||||
$numerator = $this->numerator->quotient($gcd);
|
||||
$denominator = $this->denominator->quotient($gcd);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|float|string $that) : int
|
||||
{
|
||||
return $this->minus($that)->getSign();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign() : int
|
||||
{
|
||||
return $this->numerator->getSign();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger() : BigInteger
|
||||
{
|
||||
$simplified = $this->simplified();
|
||||
|
||||
if (! $simplified->denominator->isEqualTo(1)) {
|
||||
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
|
||||
}
|
||||
|
||||
return $simplified->numerator;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal() : BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigRational() : BigRational
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toInt() : int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toFloat() : float
|
||||
{
|
||||
$simplified = $this->simplified();
|
||||
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function __toString() : string
|
||||
{
|
||||
$numerator = (string) $this->numerator;
|
||||
$denominator = (string) $this->denominator;
|
||||
|
||||
if ($denominator === '1') {
|
||||
return $numerator;
|
||||
}
|
||||
|
||||
return $numerator . '/' . $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return array{numerator: BigInteger, denominator: BigInteger}
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only here to allow unserializing the object and cannot be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||
*
|
||||
* @param array{numerator: BigInteger, denominator: BigInteger} $data
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
if (isset($this->numerator)) {
|
||||
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
$this->numerator = $data['numerator'];
|
||||
$this->denominator = $data['denominator'];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
class DivisionByZeroException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function divisionByZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function modulusMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function denominatorMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The denominator of a rational number cannot be zero.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
|
||||
/**
|
||||
* Exception thrown when an integer overflow occurs.
|
||||
*/
|
||||
class IntegerOverflowException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
|
||||
{
|
||||
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
|
||||
|
||||
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Base class for all math exceptions.
|
||||
*/
|
||||
class MathException extends \Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
|
||||
*/
|
||||
class NegativeNumberException extends MathException
|
||||
{
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||
*/
|
||||
class NumberFormatException extends MathException
|
||||
{
|
||||
public static function invalidFormat(string $value) : self
|
||||
{
|
||||
return new self(\sprintf(
|
||||
'The given value "%s" does not represent a valid number.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $char The failing character.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function charNotInAlphabet(string $char) : self
|
||||
{
|
||||
$ord = \ord($char);
|
||||
|
||||
if ($ord < 32 || $ord > 126) {
|
||||
$char = \strtoupper(\dechex($ord));
|
||||
|
||||
if ($ord < 10) {
|
||||
$char = '0' . $char;
|
||||
}
|
||||
} else {
|
||||
$char = '"' . $char . '"';
|
||||
}
|
||||
|
||||
return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
class RoundingNecessaryException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function roundingNecessary() : RoundingNecessaryException
|
||||
{
|
||||
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,668 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
/**
|
||||
* Performs basic operations on arbitrary size integers.
|
||||
*
|
||||
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
|
||||
* without leading zero, and with an optional leading minus sign if the number is not zero.
|
||||
*
|
||||
* Any other parameter format will lead to undefined behaviour.
|
||||
* All methods must return strings respecting this format, unless specified otherwise.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class Calculator
|
||||
{
|
||||
/**
|
||||
* The maximum exponent value allowed for the pow() method.
|
||||
*/
|
||||
public const MAX_POWER = 1_000_000;
|
||||
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* The Calculator instance in use.
|
||||
*/
|
||||
private static ?Calculator $instance = null;
|
||||
|
||||
/**
|
||||
* Sets the Calculator instance to use.
|
||||
*
|
||||
* An instance is typically set only in unit tests: the autodetect is usually the best option.
|
||||
*
|
||||
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
|
||||
*/
|
||||
final public static function set(?Calculator $calculator) : void
|
||||
{
|
||||
self::$instance = $calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Calculator instance to use.
|
||||
*
|
||||
* If none has been explicitly set, the fastest available implementation will be returned.
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-suppress ImpureStaticProperty
|
||||
*/
|
||||
final public static function get() : Calculator
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
/** @psalm-suppress ImpureMethodCall */
|
||||
self::$instance = self::detect();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fastest available Calculator implementation.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private static function detect() : Calculator
|
||||
{
|
||||
if (\extension_loaded('gmp')) {
|
||||
return new Calculator\GmpCalculator();
|
||||
}
|
||||
|
||||
if (\extension_loaded('bcmath')) {
|
||||
return new Calculator\BcMathCalculator();
|
||||
}
|
||||
|
||||
return new Calculator\NativeCalculator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the sign & digits of the operands.
|
||||
*
|
||||
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
|
||||
*/
|
||||
final protected function init(string $a, string $b) : array
|
||||
{
|
||||
return [
|
||||
$aNeg = ($a[0] === '-'),
|
||||
$bNeg = ($b[0] === '-'),
|
||||
|
||||
$aNeg ? \substr($a, 1) : $a,
|
||||
$bNeg ? \substr($b, 1) : $b,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of a number.
|
||||
*/
|
||||
final public function abs(string $n) : string
|
||||
{
|
||||
return ($n[0] === '-') ? \substr($n, 1) : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a number.
|
||||
*/
|
||||
final public function neg(string $n) : string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($n[0] === '-') {
|
||||
return \substr($n, 1);
|
||||
}
|
||||
|
||||
return '-' . $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* @psalm-return -1|0|1
|
||||
*
|
||||
* @return int -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
|
||||
*/
|
||||
final public function cmp(string $a, string $b) : int
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
if ($aNeg && ! $bNeg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($bNeg && ! $aNeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$aLen = \strlen($aDig);
|
||||
$bLen = \strlen($bDig);
|
||||
|
||||
if ($aLen < $bLen) {
|
||||
$result = -1;
|
||||
} elseif ($aLen > $bLen) {
|
||||
$result = 1;
|
||||
} else {
|
||||
$result = $aDig <=> $bDig;
|
||||
}
|
||||
|
||||
return $aNeg ? -$result : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two numbers.
|
||||
*/
|
||||
abstract public function add(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Subtracts two numbers.
|
||||
*/
|
||||
abstract public function sub(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Multiplies two numbers.
|
||||
*/
|
||||
abstract public function mul(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The quotient.
|
||||
*/
|
||||
abstract public function divQ(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The remainder.
|
||||
*/
|
||||
abstract public function divR(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return array{string, string} An array containing the quotient and remainder.
|
||||
*/
|
||||
abstract public function divQR(string $a, string $b) : array;
|
||||
|
||||
/**
|
||||
* Exponentiates a number.
|
||||
*
|
||||
* @param string $a The base number.
|
||||
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
|
||||
*
|
||||
* @return string The power.
|
||||
*/
|
||||
abstract public function pow(string $a, int $e) : string;
|
||||
|
||||
/**
|
||||
* @param string $b The modulus; must not be zero.
|
||||
*/
|
||||
public function mod(string $a, string $b) : string
|
||||
{
|
||||
return $this->divR($this->add($this->divR($a, $b), $b), $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modular multiplicative inverse of $x modulo $m.
|
||||
*
|
||||
* If $x has no multiplicative inverse mod m, this method must return null.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
|
||||
*
|
||||
* @param string $m The modulus; must not be negative or zero.
|
||||
*/
|
||||
public function modInverse(string $x, string $m) : ?string
|
||||
{
|
||||
if ($m === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$modVal = $x;
|
||||
|
||||
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
|
||||
$modVal = $this->mod($x, $m);
|
||||
}
|
||||
|
||||
[$g, $x] = $this->gcdExtended($modVal, $m);
|
||||
|
||||
if ($g !== '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mod($this->add($this->mod($x, $m), $m), $m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises a number into power with modulo.
|
||||
*
|
||||
* @param string $base The base number; must be positive or zero.
|
||||
* @param string $exp The exponent; must be positive or zero.
|
||||
* @param string $mod The modulus; must be strictly positive.
|
||||
*/
|
||||
abstract public function modPow(string $base, string $exp, string $mod) : string;
|
||||
|
||||
/**
|
||||
* Returns the greatest common divisor of the two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for GCD calculations.
|
||||
*
|
||||
* @return string The GCD, always positive, or zero if both arguments are zero.
|
||||
*/
|
||||
public function gcd(string $a, string $b) : string
|
||||
{
|
||||
if ($a === '0') {
|
||||
return $this->abs($b);
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $this->abs($a);
|
||||
}
|
||||
|
||||
return $this->gcd($b, $this->divR($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string, string} GCD, X, Y
|
||||
*/
|
||||
private function gcdExtended(string $a, string $b) : array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return [$b, '0', '1'];
|
||||
}
|
||||
|
||||
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
|
||||
|
||||
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
|
||||
$y = $x1;
|
||||
|
||||
return [$gcd, $x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square root of the given number, rounded down.
|
||||
*
|
||||
* The result is the largest x such that x² ≤ n.
|
||||
* The input MUST NOT be negative.
|
||||
*/
|
||||
abstract public function sqrt(string $n) : string;
|
||||
|
||||
/**
|
||||
* Converts a number from an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
|
||||
* @param int $base The base of the number, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, following the Calculator conventions.
|
||||
*/
|
||||
public function fromBase(string $number, int $base) : string
|
||||
{
|
||||
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number to convert, following the Calculator conventions.
|
||||
* @param int $base The base to convert to, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, lowercase.
|
||||
*/
|
||||
public function toBase(string $number, int $base) : string
|
||||
{
|
||||
$negative = ($number[0] === '-');
|
||||
|
||||
if ($negative) {
|
||||
$number = \substr($number, 1);
|
||||
}
|
||||
|
||||
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
|
||||
|
||||
if ($negative) {
|
||||
return '-' . $number;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
|
||||
*
|
||||
* @param string $number The number to convert, validated as a non-empty string,
|
||||
* containing only chars in the given alphabet/base.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base of the number, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The number in base 10, following the Calculator conventions.
|
||||
*/
|
||||
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||
{
|
||||
// remove leading "zeros"
|
||||
$number = \ltrim($number, $alphabet[0]);
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// optimize for "one"
|
||||
if ($number === $alphabet[1]) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
$base = (string) $base;
|
||||
|
||||
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
|
||||
$index = \strpos($alphabet, $number[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add($result, ($index === 1)
|
||||
? $power
|
||||
: $this->mul($power, (string) $index)
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, $base);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number to an arbitrary base using a custom alphabet.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base to convert to, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The converted number in the given alphabet.
|
||||
*/
|
||||
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||
{
|
||||
if ($number === '0') {
|
||||
return $alphabet[0];
|
||||
}
|
||||
|
||||
$base = (string) $base;
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, $base);
|
||||
$remainder = (int) $remainder;
|
||||
|
||||
$result .= $alphabet[$remainder];
|
||||
}
|
||||
|
||||
return \strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a rounded division.
|
||||
*
|
||||
* Rounding is performed when the remainder of the division is not zero.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
* @param RoundingMode $roundingMode The rounding mode.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the rounding mode is invalid.
|
||||
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
|
||||
*
|
||||
* @psalm-suppress ImpureFunctionCall
|
||||
*/
|
||||
final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string
|
||||
{
|
||||
[$quotient, $remainder] = $this->divQR($a, $b);
|
||||
|
||||
$hasDiscardedFraction = ($remainder !== '0');
|
||||
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
|
||||
|
||||
$discardedFractionSign = function() use ($remainder, $b) : int {
|
||||
$r = $this->abs($this->mul($remainder, '2'));
|
||||
$b = $this->abs($b);
|
||||
|
||||
return $this->cmp($r, $b);
|
||||
};
|
||||
|
||||
$increment = false;
|
||||
|
||||
switch ($roundingMode) {
|
||||
case RoundingMode::UNNECESSARY:
|
||||
if ($hasDiscardedFraction) {
|
||||
throw RoundingNecessaryException::roundingNecessary();
|
||||
}
|
||||
break;
|
||||
|
||||
case RoundingMode::UP:
|
||||
$increment = $hasDiscardedFraction;
|
||||
break;
|
||||
|
||||
case RoundingMode::DOWN:
|
||||
break;
|
||||
|
||||
case RoundingMode::CEILING:
|
||||
$increment = $hasDiscardedFraction && $isPositiveOrZero;
|
||||
break;
|
||||
|
||||
case RoundingMode::FLOOR:
|
||||
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_UP:
|
||||
$increment = $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_DOWN:
|
||||
$increment = $discardedFractionSign() > 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_CEILING:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_FLOOR:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_EVEN:
|
||||
$lastDigit = (int) $quotient[-1];
|
||||
$lastDigitIsEven = ($lastDigit % 2 === 0);
|
||||
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid rounding mode.');
|
||||
}
|
||||
|
||||
if ($increment) {
|
||||
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
|
||||
}
|
||||
|
||||
return $quotient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise AND of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*/
|
||||
public function and(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('and', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise OR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*/
|
||||
public function or(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('or', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise XOR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*/
|
||||
public function xor(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('xor', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bitwise operation on a decimal number.
|
||||
*
|
||||
* @param 'and'|'or'|'xor' $operator The operator to use.
|
||||
* @param string $a The left operand.
|
||||
* @param string $b The right operand.
|
||||
*/
|
||||
private function bitwise(string $operator, string $a, string $b) : string
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$aBin = $this->toBinary($aDig);
|
||||
$bBin = $this->toBinary($bDig);
|
||||
|
||||
$aLen = \strlen($aBin);
|
||||
$bLen = \strlen($bBin);
|
||||
|
||||
if ($aLen > $bLen) {
|
||||
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
|
||||
} elseif ($bLen > $aLen) {
|
||||
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$aBin = $this->twosComplement($aBin);
|
||||
}
|
||||
if ($bNeg) {
|
||||
$bBin = $this->twosComplement($bBin);
|
||||
}
|
||||
|
||||
$value = match ($operator) {
|
||||
'and' => $aBin & $bBin,
|
||||
'or' => $aBin | $bBin,
|
||||
'xor' => $aBin ^ $bBin,
|
||||
};
|
||||
|
||||
$negative = match ($operator) {
|
||||
'and' => $aNeg and $bNeg,
|
||||
'or' => $aNeg or $bNeg,
|
||||
'xor' => $aNeg xor $bNeg,
|
||||
};
|
||||
|
||||
if ($negative) {
|
||||
$value = $this->twosComplement($value);
|
||||
}
|
||||
|
||||
$result = $this->toDecimal($value);
|
||||
|
||||
return $negative ? $this->neg($result) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number A positive, binary number.
|
||||
*/
|
||||
private function twosComplement(string $number) : string
|
||||
{
|
||||
$xor = \str_repeat("\xff", \strlen($number));
|
||||
|
||||
$number ^= $xor;
|
||||
|
||||
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
|
||||
$byte = \ord($number[$i]);
|
||||
|
||||
if (++$byte !== 256) {
|
||||
$number[$i] = \chr($byte);
|
||||
break;
|
||||
}
|
||||
|
||||
$number[$i] = "\x00";
|
||||
|
||||
if ($i === 0) {
|
||||
$number = "\x01" . $number;
|
||||
}
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a decimal number to a binary string.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, only digits.
|
||||
*/
|
||||
private function toBinary(string $number) : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, '256');
|
||||
$result .= \chr((int) $remainder);
|
||||
}
|
||||
|
||||
return \strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the positive decimal representation of a binary number.
|
||||
*
|
||||
* @param string $bytes The bytes representing the number.
|
||||
*/
|
||||
private function toDecimal(string $bytes) : string
|
||||
{
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
|
||||
$index = \ord($bytes[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add($result, ($index === 1)
|
||||
? $power
|
||||
: $this->mul($power, (string) $index)
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, '256');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the bcmath library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class BcMathCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \bcadd($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \bcsub($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \bcmul($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \bcdiv($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
return \bcmod($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
$q = \bcdiv($a, $b, 0);
|
||||
$r = \bcmod($a, $b, 0);
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \bcpow($a, (string) $e, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \bcpowmod($base, $exp, $mod, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \bcsqrt($n, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the GMP library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class GmpCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_add($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sub($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_mul($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_q($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_r($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
[$q, $r] = \gmp_div_qr($a, $b);
|
||||
|
||||
return [
|
||||
\gmp_strval($q),
|
||||
\gmp_strval($r)
|
||||
];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_pow($a, $e));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modInverse(string $x, string $m) : ?string
|
||||
{
|
||||
$result = \gmp_invert($x, $m);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \gmp_strval($result);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_powm($base, $exp, $mod));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function gcd(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_gcd($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function fromBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_init($number, $base));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval($number, $base);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function and(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_and($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function or(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_or($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function xor(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_xor($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sqrt($n));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,598 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Calculator implementation using only native PHP code.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class NativeCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
|
||||
* For multiplication, this represents the max sum of the lengths of both operands.
|
||||
*
|
||||
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
|
||||
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
|
||||
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
|
||||
*/
|
||||
private readonly int $maxDigits;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->maxDigits = match (PHP_INT_SIZE) {
|
||||
4 => 9,
|
||||
8 => 18,
|
||||
default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.')
|
||||
};
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
/**
|
||||
* @psalm-var numeric-string $a
|
||||
* @psalm-var numeric-string $b
|
||||
*/
|
||||
$result = $a + $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
|
||||
|
||||
if ($aNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return $this->add($a, $this->neg($b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
/**
|
||||
* @psalm-var numeric-string $a
|
||||
* @psalm-var numeric-string $b
|
||||
*/
|
||||
$result = $a * $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0' || $b === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($a === '1') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
if ($a === '-1') {
|
||||
return $this->neg($b);
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return $this->neg($a);
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $this->doMul($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return $this->divQR($a, $b)[0];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[1];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return ['0', '0'];
|
||||
}
|
||||
|
||||
if ($a === $b) {
|
||||
return ['1', '0'];
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return [$a, '0'];
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return [$this->neg($a), '0'];
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $a */
|
||||
$na = $a * 1; // cast to number
|
||||
|
||||
if (is_int($na)) {
|
||||
/** @psalm-var numeric-string $b */
|
||||
$nb = $b * 1;
|
||||
|
||||
if (is_int($nb)) {
|
||||
// the only division that may overflow is PHP_INT_MIN / -1,
|
||||
// which cannot happen here as we've already handled a divisor of -1 above.
|
||||
$q = intdiv($na, $nb);
|
||||
$r = $na % $nb;
|
||||
|
||||
return [
|
||||
(string) $q,
|
||||
(string) $r
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
[$q, $r] = $this->doDiv($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$q = $this->neg($q);
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$r = $this->neg($r);
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
if ($e === 0) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
if ($e === 1) {
|
||||
return $a;
|
||||
}
|
||||
|
||||
$odd = $e % 2;
|
||||
$e -= $odd;
|
||||
|
||||
$aa = $this->mul($a, $a);
|
||||
|
||||
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
|
||||
$result = $this->pow($aa, $e / 2);
|
||||
|
||||
if ($odd === 1) {
|
||||
$result = $this->mul($result, $a);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
|
||||
*/
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($base === '0' && $exp === '0' && $mod === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($exp === '0' && $mod === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$x = $base;
|
||||
|
||||
$res = '1';
|
||||
|
||||
// numbers are positive, so we can use remainder instead of modulo
|
||||
$x = $this->divR($x, $mod);
|
||||
|
||||
while ($exp !== '0') {
|
||||
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
|
||||
$res = $this->divR($this->mul($res, $x), $mod);
|
||||
}
|
||||
|
||||
$exp = $this->divQ($exp, '2');
|
||||
$x = $this->divR($this->mul($x, $x), $mod);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
|
||||
*/
|
||||
#[Override]
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// initial approximation
|
||||
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
|
||||
|
||||
$decreased = false;
|
||||
|
||||
for (;;) {
|
||||
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
|
||||
|
||||
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decreased = $this->cmp($nx, $x) < 0;
|
||||
$x = $nx;
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the addition of two non-signed large integers.
|
||||
*/
|
||||
private function doAdd(string $a, string $b) : string
|
||||
{
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $blockA */
|
||||
$blockA = \substr($a, $i, $blockLength);
|
||||
|
||||
/** @psalm-var numeric-string $blockB */
|
||||
$blockB = \substr($b, $i, $blockLength);
|
||||
|
||||
$sum = (string) ($blockA + $blockB + $carry);
|
||||
$sumLength = \strlen($sum);
|
||||
|
||||
if ($sumLength > $blockLength) {
|
||||
$sum = \substr($sum, 1);
|
||||
$carry = 1;
|
||||
} else {
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry === 1) {
|
||||
$result = '1' . $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the subtraction of two non-signed large integers.
|
||||
*/
|
||||
private function doSub(string $a, string $b) : string
|
||||
{
|
||||
if ($a === $b) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Ensure that we always subtract to a positive result: biggest minus smallest.
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
$invert = ($cmp === -1);
|
||||
|
||||
if ($invert) {
|
||||
$c = $a;
|
||||
$a = $b;
|
||||
$b = $c;
|
||||
}
|
||||
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
$complement = 10 ** $this->maxDigits;
|
||||
|
||||
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $blockA */
|
||||
$blockA = \substr($a, $i, $blockLength);
|
||||
|
||||
/** @psalm-var numeric-string $blockB */
|
||||
$blockB = \substr($b, $i, $blockLength);
|
||||
|
||||
$sum = $blockA - $blockB - $carry;
|
||||
|
||||
if ($sum < 0) {
|
||||
$sum += $complement;
|
||||
$carry = 1;
|
||||
} else {
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$sum = (string) $sum;
|
||||
$sumLength = \strlen($sum);
|
||||
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Carry cannot be 1 when the loop ends, as a > b
|
||||
assert($carry === 0);
|
||||
|
||||
$result = \ltrim($result, '0');
|
||||
|
||||
if ($invert) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the multiplication of two non-signed large integers.
|
||||
*/
|
||||
private function doMul(string $a, string $b) : string
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
$maxDigits = \intdiv($this->maxDigits, 2);
|
||||
$complement = 10 ** $maxDigits;
|
||||
|
||||
$result = '0';
|
||||
|
||||
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
|
||||
$blockALength = $maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockALength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
$blockA = (int) \substr($a, $i, $blockALength);
|
||||
|
||||
$line = '';
|
||||
$carry = 0;
|
||||
|
||||
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
|
||||
$blockBLength = $maxDigits;
|
||||
|
||||
if ($j < 0) {
|
||||
$blockBLength += $j;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$j = 0;
|
||||
}
|
||||
|
||||
$blockB = (int) \substr($b, $j, $blockBLength);
|
||||
|
||||
$mul = $blockA * $blockB + $carry;
|
||||
$value = $mul % $complement;
|
||||
$carry = ($mul - $value) / $complement;
|
||||
|
||||
$value = (string) $value;
|
||||
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
|
||||
|
||||
$line = $value . $line;
|
||||
|
||||
if ($j === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry !== 0) {
|
||||
$line = $carry . $line;
|
||||
}
|
||||
|
||||
$line = \ltrim($line, '0');
|
||||
|
||||
if ($line !== '') {
|
||||
$line .= \str_repeat('0', $x - $blockALength - $i);
|
||||
$result = $this->add($result, $line);
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the division of two non-signed large integers.
|
||||
*
|
||||
* @return string[] The quotient and remainder.
|
||||
*/
|
||||
private function doDiv(string $a, string $b) : array
|
||||
{
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
return ['0', $a];
|
||||
}
|
||||
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
// we now know that a >= b && x >= y
|
||||
|
||||
$q = '0'; // quotient
|
||||
$r = $a; // remainder
|
||||
$z = $y; // focus length, always $y or $y+1
|
||||
|
||||
/** @psalm-var numeric-string $b */
|
||||
$nb = $b * 1; // cast to number
|
||||
// performance optimization in cases where the remainder will never cause int overflow
|
||||
if (is_int(($nb - 1) * 10 + 9)) {
|
||||
$r = (int) \substr($a, 0, $z - 1);
|
||||
|
||||
for ($i = $z - 1; $i < $x; $i++) {
|
||||
$n = $r * 10 + (int) $a[$i];
|
||||
/** @psalm-var int $nb */
|
||||
$q .= \intdiv($n, $nb);
|
||||
$r = $n % $nb;
|
||||
}
|
||||
|
||||
return [\ltrim($q, '0') ?: '0', (string) $r];
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
$focus = \substr($a, 0, $z);
|
||||
|
||||
$cmp = $this->doCmp($focus, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
if ($z === $x) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z++;
|
||||
}
|
||||
|
||||
$zeros = \str_repeat('0', $x - $z);
|
||||
|
||||
$q = $this->add($q, '1' . $zeros);
|
||||
$a = $this->sub($a, $b . $zeros);
|
||||
|
||||
$r = $a;
|
||||
|
||||
if ($r === '0') { // remainder == 0
|
||||
break;
|
||||
}
|
||||
|
||||
$x = \strlen($a);
|
||||
|
||||
if ($x < $y) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z = $y;
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two non-signed large numbers.
|
||||
*
|
||||
* @psalm-return -1|0|1
|
||||
*/
|
||||
private function doCmp(string $a, string $b) : int
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
$cmp = $x <=> $y;
|
||||
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
return \strcmp($a, $b) <=> 0; // enforce -1|0|1
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
|
||||
*
|
||||
* The numbers must only consist of digits, without leading minus sign.
|
||||
*
|
||||
* @return array{string, string, int}
|
||||
*/
|
||||
private function pad(string $a, string $b) : array
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
if ($x > $y) {
|
||||
$b = \str_repeat('0', $x - $y) . $b;
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
|
||||
if ($x < $y) {
|
||||
$a = \str_repeat('0', $y - $x) . $a;
|
||||
|
||||
return [$a, $b, $y];
|
||||
}
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
/**
|
||||
* Specifies a rounding behavior for numerical operations capable of discarding precision.
|
||||
*
|
||||
* Each rounding mode indicates how the least significant returned digit of a rounded result
|
||||
* is to be calculated. If fewer digits are returned than the digits needed to represent the
|
||||
* exact numerical result, the discarded digits will be referred to as the discarded fraction
|
||||
* regardless the digits' contribution to the value of the number. In other words, considered
|
||||
* as a numerical value, the discarded fraction could have an absolute value greater than one.
|
||||
*/
|
||||
enum RoundingMode
|
||||
{
|
||||
/**
|
||||
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
|
||||
*
|
||||
* If this rounding mode is specified on an operation that yields a result that
|
||||
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
|
||||
*/
|
||||
case UNNECESSARY;
|
||||
|
||||
/**
|
||||
* Rounds away from zero.
|
||||
*
|
||||
* Always increments the digit prior to a nonzero discarded fraction.
|
||||
* Note that this rounding mode never decreases the magnitude of the calculated value.
|
||||
*/
|
||||
case UP;
|
||||
|
||||
/**
|
||||
* Rounds towards zero.
|
||||
*
|
||||
* Never increments the digit prior to a discarded fraction (i.e., truncates).
|
||||
* Note that this rounding mode never increases the magnitude of the calculated value.
|
||||
*/
|
||||
case DOWN;
|
||||
|
||||
/**
|
||||
* Rounds towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
|
||||
* Note that this rounding mode never decreases the calculated value.
|
||||
*/
|
||||
case CEILING;
|
||||
|
||||
/**
|
||||
* Rounds towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
|
||||
* Note that this rounding mode never increases the calculated value.
|
||||
*/
|
||||
case FLOOR;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
|
||||
* Note that this is the rounding mode commonly taught at school.
|
||||
*/
|
||||
case HALF_UP;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
|
||||
*/
|
||||
case HALF_DOWN;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
|
||||
*/
|
||||
case HALF_CEILING;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
|
||||
*/
|
||||
case HALF_FLOOR;
|
||||
|
||||
/**
|
||||
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
|
||||
*
|
||||
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
|
||||
* behaves as for HALF_DOWN if it's even.
|
||||
*
|
||||
* Note that this is the rounding mode that statistically minimizes
|
||||
* cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
|
||||
*/
|
||||
case HALF_EVEN;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Carbon
|
||||
|
||||
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 @@
|
|||
# carbonphp/carbon-doctrine-types
|
||||
|
||||
Types to use Carbon in Doctrine
|
||||
|
||||
## Documentation
|
||||
|
||||
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
|
||||
|
||||
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
|
||||
from `nestbot/carbon` package.
|
||||
|
||||
Externalization allows to better deal with different versions of dbal. With
|
||||
version 4.0 of dbal, it no longer sustainable to be compatible with all version
|
||||
using a single code.
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"description": "Types to use Carbon in Doctrine",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"date",
|
||||
"time",
|
||||
"DateTime",
|
||||
"Carbon",
|
||||
"Doctrine"
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^3.7.0",
|
||||
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||
"phpunit/phpunit": "^10.3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<3.7.0 || >=4.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "KyleKatarn",
|
||||
"email": "kylekatarnls@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
}
|
14
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
14
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
interface CarbonDoctrineType
|
||||
{
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform);
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform);
|
||||
}
|
7
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
7
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonType extends DateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
141
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
141
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\DB2Platform;
|
||||
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @template T of CarbonInterface
|
||||
*/
|
||||
trait CarbonTypeConverter
|
||||
{
|
||||
/**
|
||||
* This property differentiates types installed by carbonphp/carbon-doctrine-types
|
||||
* from the ones embedded previously in nesbot/carbon source directly.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public bool $external = true;
|
||||
|
||||
/**
|
||||
* @return class-string<T>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return Carbon::class;
|
||||
}
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
$precision = min(
|
||||
$fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
|
||||
$this->getMaximumPrecision($platform),
|
||||
);
|
||||
|
||||
$type = parent::getSQLDeclaration($fieldDeclaration, $platform);
|
||||
|
||||
if (!$precision) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (str_contains($type, '(')) {
|
||||
return preg_replace('/\(\d+\)/', "($precision)", $type);
|
||||
}
|
||||
|
||||
[$before, $after] = explode(' ', "$type ");
|
||||
|
||||
return trim("$before($precision) $after");
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
$class = $this->getCarbonClassName();
|
||||
|
||||
if ($value === null || is_a($value, $class)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $class::instance($value);
|
||||
}
|
||||
|
||||
$date = null;
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
$date = $class::parse($value);
|
||||
} catch (Exception $exception) {
|
||||
$error = $exception;
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
$value,
|
||||
$this->getTypeName(),
|
||||
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
|
||||
$error
|
||||
);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
|
||||
throw ConversionException::conversionFailedInvalidType(
|
||||
$value,
|
||||
$this->getTypeName(),
|
||||
['null', 'DateTime', 'Carbon']
|
||||
);
|
||||
}
|
||||
|
||||
private function getTypeName(): string
|
||||
{
|
||||
$chunks = explode('\\', static::class);
|
||||
$type = preg_replace('/Type$/', '', end($chunks));
|
||||
|
||||
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $type));
|
||||
}
|
||||
|
||||
private function getMaximumPrecision(AbstractPlatform $platform): int
|
||||
{
|
||||
if ($platform instanceof DB2Platform) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
if ($platform instanceof OraclePlatform) {
|
||||
return 9;
|
||||
}
|
||||
|
||||
if ($platform instanceof SQLServerPlatform || $platform instanceof SqlitePlatform) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
28
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
28
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class DateTimeDefaultPrecision
|
||||
{
|
||||
private static $precision = 6;
|
||||
|
||||
/**
|
||||
* Change the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @param int $precision
|
||||
*/
|
||||
public static function set(int $precision): void
|
||||
{
|
||||
self::$precision = $precision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get(): int
|
||||
{
|
||||
return self::$precision;
|
||||
}
|
||||
}
|
20
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
20
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
|
||||
|
||||
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<CarbonImmutable> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @return class-string<CarbonImmutable>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return CarbonImmutable::class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Doctrine\DBAL\Types\VarDateTimeType;
|
||||
|
||||
class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<Carbon> */
|
||||
use CarbonTypeConverter;
|
||||
}
|
|
@ -0,0 +1,579 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
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.
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php',
|
||||
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
|
||||
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
|
||||
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
|
||||
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
|
||||
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
|
||||
'f57d353b41eb2e234b26064d63d8c5dd' => $vendorDir . '/laravel/framework/src/Illuminate/Filesystem/functions.php',
|
||||
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
|
||||
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
|
||||
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
|
||||
'a1cfe24d14977df6878b9bf804af2d1c' => $vendorDir . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',
|
||||
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
|
||||
'320163ac6b93aebe3dc25b60a0533d56' => $vendorDir . '/spatie/laravel-ignition/src/helpers.php',
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
|
||||
);
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
|
||||
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
|
||||
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
|
||||
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
|
||||
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
|
||||
'Tests\\' => array($baseDir . '/tests'),
|
||||
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
|
||||
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
|
||||
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
|
||||
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
|
||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
|
||||
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
|
||||
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
|
||||
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
|
||||
'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'),
|
||||
'Symfony\\Component\\Uid\\' => array($vendorDir . '/symfony/uid'),
|
||||
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
|
||||
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
|
||||
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
|
||||
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
|
||||
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
|
||||
'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'),
|
||||
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
|
||||
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
|
||||
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
|
||||
'Symfony\\Component\\ErrorHandler\\' => array($vendorDir . '/symfony/error-handler'),
|
||||
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
|
||||
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
|
||||
'Svg\\' => array($vendorDir . '/dompdf/php-svg-lib/src/Svg'),
|
||||
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/error-solutions/legacy/laravel-ignition', $vendorDir . '/spatie/laravel-ignition/src'),
|
||||
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/error-solutions/legacy/ignition', $vendorDir . '/spatie/ignition/src'),
|
||||
'Spatie\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'),
|
||||
'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'),
|
||||
'Spatie\\Backtrace\\' => array($vendorDir . '/spatie/backtrace/src'),
|
||||
'Sabberworm\\CSS\\' => array($vendorDir . '/sabberworm/php-css-parser/src'),
|
||||
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
|
||||
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
|
||||
'Psy\\' => array($vendorDir . '/psy/psysh/src'),
|
||||
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
|
||||
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
||||
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'),
|
||||
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
|
||||
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
|
||||
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
|
||||
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
|
||||
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
|
||||
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
|
||||
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
|
||||
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
|
||||
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
|
||||
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
|
||||
'Maatwebsite\\Excel\\' => array($vendorDir . '/maatwebsite/excel/src'),
|
||||
'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\\Config\\' => array($vendorDir . '/league/config/src'),
|
||||
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
|
||||
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
|
||||
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
|
||||
'Laravel\\Sanctum\\' => array($vendorDir . '/laravel/sanctum/src'),
|
||||
'Laravel\\Sail\\' => array($vendorDir . '/laravel/sail/src'),
|
||||
'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
|
||||
'Intervention\\Image\\' => array($vendorDir . '/intervention/image/src'),
|
||||
'Intervention\\Gif\\' => array($vendorDir . '/intervention/gif/src'),
|
||||
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable'),
|
||||
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
|
||||
'GuzzleHttp\\UriTemplate\\' => array($vendorDir . '/guzzlehttp/uri-template/src'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
|
||||
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/php-cors/src'),
|
||||
'FontLib\\' => array($vendorDir . '/dompdf/php-font-lib/src/FontLib'),
|
||||
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
|
||||
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
|
||||
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
|
||||
'Dompdf\\' => array($vendorDir . '/dompdf/dompdf/src'),
|
||||
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib/Doctrine/Inflector'),
|
||||
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
|
||||
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
|
||||
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
|
||||
'Database\\Seeders\\' => array($baseDir . '/database/seeders', $vendorDir . '/laravel/pint/database/seeders'),
|
||||
'Database\\Factories\\' => array($baseDir . '/database/factories', $vendorDir . '/laravel/pint/database/factories'),
|
||||
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
|
||||
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
|
||||
'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
|
||||
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
|
||||
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
|
||||
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
|
||||
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
|
||||
'Barryvdh\\DomPDF\\' => array($vendorDir . '/barryvdh/laravel-dompdf/src'),
|
||||
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),
|
||||
);
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInita29a979da9754defe6e0c8ead1270f52
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInita29a979da9754defe6e0c8ead1270f52', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInita29a979da9754defe6e0c8ead1270f52', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInita29a979da9754defe6e0c8ead1270f52::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInita29a979da9754defe6e0c8ead1270f52::$files;
|
||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}, null, null);
|
||||
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||
$requireFile($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
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
|
@ -0,0 +1,19 @@
|
|||
Copyright (C) 2021 Composer
|
||||
|
||||
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,189 @@
|
|||
composer/pcre
|
||||
=============
|
||||
|
||||
PCRE wrapping library that offers type-safe `preg_*` replacements.
|
||||
|
||||
This library gives you a way to ensure `preg_*` functions do not fail silently, returning
|
||||
unexpected `null`s that may not be handled.
|
||||
|
||||
As of 3.0 this library enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage
|
||||
for all matching and replaceCallback functions, [read more below](#preg_unmatched_as_null)
|
||||
to understand the implications.
|
||||
|
||||
It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it
|
||||
simplifies and reduces the possible return values from all the `preg_*` functions which
|
||||
are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a
|
||||
[PHPStan extension](#phpstan-extension) for parsing regular expressions and giving you even better output types.
|
||||
|
||||
This library is a thin wrapper around `preg_*` functions with [some limitations](#restrictions--limitations).
|
||||
If you are looking for a richer API to handle regular expressions have a look at
|
||||
[rawr/t-regx](https://packagist.org/packages/rawr/t-regx) instead.
|
||||
|
||||
[](https://github.com/composer/pcre/actions)
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install the latest version with:
|
||||
|
||||
```bash
|
||||
$ composer require composer/pcre
|
||||
```
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* PHP 7.4.0 is required for 3.x versions
|
||||
* PHP 7.2.0 is required for 2.x versions
|
||||
* PHP 5.3.2 is required for 1.x versions
|
||||
|
||||
|
||||
Basic usage
|
||||
-----------
|
||||
|
||||
Instead of:
|
||||
|
||||
```php
|
||||
if (preg_match('{fo+}', $string, $matches)) { ... }
|
||||
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
|
||||
if (preg_match_all('{fo+}', $string, $matches)) { ... }
|
||||
$newString = preg_replace('{fo+}', 'bar', $string);
|
||||
$newString = preg_replace_callback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
|
||||
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
|
||||
$filtered = preg_grep('{[a-z]}', $elements);
|
||||
$array = preg_split('{[a-z]+}', $string);
|
||||
```
|
||||
|
||||
You can now call these on the `Preg` class:
|
||||
|
||||
```php
|
||||
use Composer\Pcre\Preg;
|
||||
|
||||
if (Preg::match('{fo+}', $string, $matches)) { ... }
|
||||
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
|
||||
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
|
||||
$newString = Preg::replace('{fo+}', 'bar', $string);
|
||||
$newString = Preg::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
|
||||
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
|
||||
$filtered = Preg::grep('{[a-z]}', $elements);
|
||||
$array = Preg::split('{[a-z]+}', $string);
|
||||
```
|
||||
|
||||
The main difference is if anything fails to match/replace/.., it will throw a `Composer\Pcre\PcreException`
|
||||
instead of returning `null` (or false in some cases), so you can now use the return values safely relying on
|
||||
the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).
|
||||
|
||||
Additionally the `Preg` class provides match methods that return `bool` rather than `int`, for stricter type safety
|
||||
when the number of pattern matches is not useful:
|
||||
|
||||
```php
|
||||
use Composer\Pcre\Preg;
|
||||
|
||||
if (Preg::isMatch('{fo+}', $string, $matches)) // bool
|
||||
if (Preg::isMatchAll('{fo+}', $string, $matches)) // bool
|
||||
```
|
||||
|
||||
Finally the `Preg` class provides a few `*StrictGroups` method variants that ensure match groups
|
||||
are always present and thus non-nullable, making it easier to write type-safe code:
|
||||
|
||||
```php
|
||||
use Composer\Pcre\Preg;
|
||||
|
||||
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throw
|
||||
if (Preg::matchStrictGroups('{fo+}', $string, $matches))
|
||||
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))
|
||||
```
|
||||
|
||||
**Note:** This is generally safe to use as long as you do not have optional subpatterns (i.e. `(something)?`
|
||||
or `(something)*` or branches with a `|` that result in some groups not being matched at all).
|
||||
A subpattern that can match an empty string like `(.*)` is **not** optional, it will be present as an
|
||||
empty string in the matches. A non-matching subpattern, even if optional like `(?:foo)?` will anyway not be present in
|
||||
matches so it is also not a problem to use these with `*StrictGroups` methods.
|
||||
|
||||
If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the `Regex` class:
|
||||
|
||||
```php
|
||||
use Composer\Pcre\Regex;
|
||||
|
||||
// this is useful when you are just interested in knowing if something matched
|
||||
// as it returns a bool instead of int(1/0) for match
|
||||
$bool = Regex::isMatch('{fo+}', $string);
|
||||
|
||||
$result = Regex::match('{fo+}', $string);
|
||||
if ($result->matched) { something($result->matches); }
|
||||
|
||||
$result = Regex::matchWithOffsets('{fo+}', $string);
|
||||
if ($result->matched) { something($result->matches); }
|
||||
|
||||
$result = Regex::matchAll('{fo+}', $string);
|
||||
if ($result->matched && $result->count > 3) { something($result->matches); }
|
||||
|
||||
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
|
||||
$newString = Regex::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string)->result;
|
||||
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;
|
||||
```
|
||||
|
||||
Note that `preg_grep` and `preg_split` are only callable via the `Preg` class as they do not have
|
||||
complex return types warranting a specific result object.
|
||||
|
||||
See the [MatchResult](src/MatchResult.php), [MatchWithOffsetsResult](src/MatchWithOffsetsResult.php), [MatchAllResult](src/MatchAllResult.php),
|
||||
[MatchAllWithOffsetsResult](src/MatchAllWithOffsetsResult.php), and [ReplaceResult](src/ReplaceResult.php) class sources for more details.
|
||||
|
||||
Restrictions / Limitations
|
||||
--------------------------
|
||||
|
||||
Due to type safety requirements a few restrictions are in place.
|
||||
|
||||
- matching using `PREG_OFFSET_CAPTURE` is made available via `matchWithOffsets` and `matchAllWithOffsets`.
|
||||
You cannot pass the flag to `match`/`matchAll`.
|
||||
- `Preg::split` will also reject `PREG_SPLIT_OFFSET_CAPTURE` and you should use `splitWithOffsets`
|
||||
instead.
|
||||
- `matchAll` rejects `PREG_SET_ORDER` as it also changes the shape of the returned matches. There
|
||||
is no alternative provided as you can fairly easily code around it.
|
||||
- `preg_filter` is not supported as it has a rather crazy API, most likely you should rather
|
||||
use `Preg::grep` in combination with some loop and `Preg::replace`.
|
||||
- `replace`, `replaceCallback` and `replaceCallbackArray` do not support an array `$subject`,
|
||||
only simple strings.
|
||||
- As of 2.0, the library always uses `PREG_UNMATCHED_AS_NULL` for matching, which offers [much
|
||||
saner/more predictable results](#preg_unmatched_as_null). As of 3.0 the flag is also set for
|
||||
`replaceCallback` and `replaceCallbackArray`.
|
||||
|
||||
#### PREG_UNMATCHED_AS_NULL
|
||||
|
||||
As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all `match*` and `isMatch*`
|
||||
functions. As of 3.0 it is also done for `replaceCallback` and `replaceCallbackArray`.
|
||||
|
||||
This means your matches will always contain all matching groups, either as null if unmatched
|
||||
or as string if it matched.
|
||||
|
||||
The advantages in clarity and predictability are clearer if you compare the two outputs of
|
||||
running this with and without PREG_UNMATCHED_AS_NULL in $flags:
|
||||
|
||||
```php
|
||||
preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
|
||||
```
|
||||
|
||||
| no flag | PREG_UNMATCHED_AS_NULL |
|
||||
| --- | --- |
|
||||
| array (size=4) | array (size=5) |
|
||||
| 0 => string 'ac' (length=2) | 0 => string 'ac' (length=2) |
|
||||
| 1 => string 'a' (length=1) | 1 => string 'a' (length=1) |
|
||||
| 2 => string '' (length=0) | 2 => null |
|
||||
| 3 => string 'c' (length=1) | 3 => string 'c' (length=1) |
|
||||
| | 4 => null |
|
||||
| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for |
|
||||
| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` |
|
||||
|
||||
PHPStan Extension
|
||||
-----------------
|
||||
|
||||
To use the PHPStan extension if you do not use `phpstan/extension-installer` you can include `vendor/composer/pcre/extension.neon` in your PHPStan config.
|
||||
|
||||
The extension provides much better type information for $matches as well as regex validation where possible.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
composer/pcre is licensed under the MIT License, see the LICENSE file for details.
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "composer/pcre",
|
||||
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"pcre",
|
||||
"regex",
|
||||
"preg",
|
||||
"regular expression"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"phpstan/phpstan": "^1.12 || ^2",
|
||||
"phpstan/phpstan-strict-rules": "^1 || ^2"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\Pcre\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Composer\\Pcre\\": "tests"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
},
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "@php vendor/bin/phpunit",
|
||||
"phpstan": "@php phpstan analyse"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
# composer/pcre PHPStan extensions
|
||||
#
|
||||
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
|
||||
# in your phpstan config
|
||||
|
||||
services:
|
||||
-
|
||||
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
|
||||
tags:
|
||||
- phpstan.staticMethodParameterOutTypeExtension
|
||||
-
|
||||
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
|
||||
tags:
|
||||
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
|
||||
-
|
||||
class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension
|
||||
tags:
|
||||
- phpstan.staticMethodParameterClosureTypeExtension
|
||||
|
||||
rules:
|
||||
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
|
||||
- Composer\Pcre\PHPStan\InvalidRegexPatternRule
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchAllResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => list of matched strings
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, list<string|null>>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var 0|positive-int
|
||||
*/
|
||||
public $count;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<int|string, list<string|null>> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchAllStrictGroupsResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => list of matched strings
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, list<string>>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var 0|positive-int
|
||||
*/
|
||||
public $count;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<list<string>> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchAllWithOffsetsResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => list of matches, every match being a pair of string matched + offset in bytes (or -1 if no match)
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, list<array{string|null, int}>>
|
||||
* @phpstan-var array<int|string, list<array{string|null, int<-1, max>}>>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var 0|positive-int
|
||||
*/
|
||||
public $count;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<int|string, list<array{string|null, int}>> $matches
|
||||
* @phpstan-param array<int|string, list<array{string|null, int<-1, max>}>> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => string matched
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, string|null>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<string|null> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchStrictGroupsResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => string matched
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, string>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<string> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of composer/pcre.
|
||||
*
|
||||
* (c) Composer <https://github.com/composer>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Pcre;
|
||||
|
||||
final class MatchWithOffsetsResult
|
||||
{
|
||||
/**
|
||||
* An array of match group => pair of string matched + offset in bytes (or -1 if no match)
|
||||
*
|
||||
* @readonly
|
||||
* @var array<int|string, array{string|null, int}>
|
||||
* @phpstan-var array<int|string, array{string|null, int<-1, max>}>
|
||||
*/
|
||||
public $matches;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @var bool
|
||||
*/
|
||||
public $matched;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $count
|
||||
* @param array<array{string|null, int}> $matches
|
||||
* @phpstan-param array<int|string, array{string|null, int<-1, max>}> $matches
|
||||
*/
|
||||
public function __construct(int $count, array $matches)
|
||||
{
|
||||
$this->matches = $matches;
|
||||
$this->matched = (bool) $count;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace Composer\Pcre\PHPStan;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use Composer\Pcre\Regex;
|
||||
use Composer\Pcre\PcreException;
|
||||
use Nette\Utils\RegexpException;
|
||||
use Nette\Utils\Strings;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Name\FullyQualified;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Rules\Rule;
|
||||
use PHPStan\Rules\RuleErrorBuilder;
|
||||
use function in_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Copy of PHPStan's RegularExpressionPatternRule
|
||||
*
|
||||
* @implements Rule<StaticCall>
|
||||
*/
|
||||
class InvalidRegexPatternRule implements Rule
|
||||
{
|
||||
public function getNodeType(): string
|
||||
{
|
||||
return StaticCall::class;
|
||||
}
|
||||
|
||||
public function processNode(Node $node, Scope $scope): array
|
||||
{
|
||||
$patterns = $this->extractPatterns($node, $scope);
|
||||
|
||||
$errors = [];
|
||||
foreach ($patterns as $pattern) {
|
||||
$errorMessage = $this->validatePattern($pattern);
|
||||
if ($errorMessage === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build();
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractPatterns(StaticCall $node, Scope $scope): array
|
||||
{
|
||||
if (!$node->class instanceof FullyQualified) {
|
||||
return [];
|
||||
}
|
||||
$isRegex = $node->class->toString() === Regex::class;
|
||||
$isPreg = $node->class->toString() === Preg::class;
|
||||
if (!$isRegex && !$isPreg) {
|
||||
return [];
|
||||
}
|
||||
if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$functionName = $node->name->name;
|
||||
if (!isset($node->getArgs()[0])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$patternNode = $node->getArgs()[0]->value;
|
||||
$patternType = $scope->getType($patternNode);
|
||||
|
||||
$patternStrings = [];
|
||||
|
||||
foreach ($patternType->getConstantStrings() as $constantStringType) {
|
||||
if ($functionName === 'replaceCallbackArray') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$patternStrings[] = $constantStringType->getValue();
|
||||
}
|
||||
|
||||
foreach ($patternType->getConstantArrays() as $constantArrayType) {
|
||||
if (
|
||||
in_array($functionName, [
|
||||
'replace',
|
||||
'replaceCallback',
|
||||
], true)
|
||||
) {
|
||||
foreach ($constantArrayType->getValueTypes() as $arrayKeyType) {
|
||||
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
|
||||
$patternStrings[] = $constantString->getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($functionName !== 'replaceCallbackArray') {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) {
|
||||
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
|
||||
$patternStrings[] = $constantString->getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $patternStrings;
|
||||
}
|
||||
|
||||
private function validatePattern(string $pattern): ?string
|
||||
{
|
||||
try {
|
||||
$msg = null;
|
||||
$prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool {
|
||||
$msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($pattern === '') {
|
||||
return 'Empty string is not a valid regular expression';
|
||||
}
|
||||
|
||||
Preg::match($pattern, '');
|
||||
if ($msg !== null) {
|
||||
return $msg;
|
||||
}
|
||||
} catch (PcreException $e) {
|
||||
if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) {
|
||||
return $msg;
|
||||
}
|
||||
|
||||
return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage());
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Pcre\PHPStan;
|
||||
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\Constant\ConstantArrayType;
|
||||
use PHPStan\Type\Constant\ConstantIntegerType;
|
||||
use PHPStan\Type\IntersectionType;
|
||||
use PHPStan\Type\TypeCombinator;
|
||||
use PHPStan\Type\Type;
|
||||
use PhpParser\Node\Arg;
|
||||
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||
use PHPStan\Type\TypeTraverser;
|
||||
use PHPStan\Type\UnionType;
|
||||
|
||||
final class PregMatchFlags
|
||||
{
|
||||
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
|
||||
{
|
||||
if ($flagsArg === null) {
|
||||
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
|
||||
}
|
||||
|
||||
$flagsType = $scope->getType($flagsArg->value);
|
||||
|
||||
$constantScalars = $flagsType->getConstantScalarValues();
|
||||
if ($constantScalars === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$internalFlagsTypes = [];
|
||||
foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) {
|
||||
if (!is_int($constantScalarValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL);
|
||||
}
|
||||
return TypeCombinator::union(...$internalFlagsTypes);
|
||||
}
|
||||
|
||||
static public function removeNullFromMatches(Type $matchesType): Type
|
||||
{
|
||||
return TypeTraverser::map($matchesType, static function (Type $type, callable $traverse): Type {
|
||||
if ($type instanceof UnionType || $type instanceof IntersectionType) {
|
||||
return $traverse($type);
|
||||
}
|
||||
|
||||
if ($type instanceof ConstantArrayType) {
|
||||
return new ConstantArrayType(
|
||||
$type->getKeyTypes(),
|
||||
array_map(static function (Type $valueType) use ($traverse): Type {
|
||||
return $traverse($valueType);
|
||||
}, $type->getValueTypes()),
|
||||
$type->getNextAutoIndexes(),
|
||||
[],
|
||||
$type->isList()
|
||||
);
|
||||
}
|
||||
|
||||
if ($type instanceof ArrayType) {
|
||||
return new ArrayType($type->getKeyType(), $traverse($type->getItemType()));
|
||||
}
|
||||
|
||||
return TypeCombinator::removeNull($type);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Pcre\PHPStan;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Reflection\MethodReflection;
|
||||
use PHPStan\Reflection\ParameterReflection;
|
||||
use PHPStan\TrinaryLogic;
|
||||
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
|
||||
use PHPStan\Type\Type;
|
||||
|
||||
final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
|
||||
{
|
||||
/**
|
||||
* @var RegexArrayShapeMatcher
|
||||
*/
|
||||
private $regexShapeMatcher;
|
||||
|
||||
public function __construct(
|
||||
RegexArrayShapeMatcher $regexShapeMatcher
|
||||
)
|
||||
{
|
||||
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||
}
|
||||
|
||||
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
|
||||
{
|
||||
return
|
||||
$methodReflection->getDeclaringClass()->getName() === Preg::class
|
||||
&& in_array($methodReflection->getName(), [
|
||||
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
|
||||
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
|
||||
], true)
|
||||
&& $parameter->getName() === 'matches';
|
||||
}
|
||||
|
||||
public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
|
||||
{
|
||||
$args = $methodCall->getArgs();
|
||||
$patternArg = $args[0] ?? null;
|
||||
$matchesArg = $args[2] ?? null;
|
||||
$flagsArg = $args[3] ?? null;
|
||||
|
||||
if (
|
||||
$patternArg === null || $matchesArg === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||
if ($flagsType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
|
||||
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
|
||||
}
|
||||
|
||||
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Pcre\PHPStan;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Analyser\SpecifiedTypes;
|
||||
use PHPStan\Analyser\TypeSpecifier;
|
||||
use PHPStan\Analyser\TypeSpecifierAwareExtension;
|
||||
use PHPStan\Analyser\TypeSpecifierContext;
|
||||
use PHPStan\Reflection\MethodReflection;
|
||||
use PHPStan\TrinaryLogic;
|
||||
use PHPStan\Type\Constant\ConstantArrayType;
|
||||
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
|
||||
use PHPStan\Type\TypeCombinator;
|
||||
use PHPStan\Type\Type;
|
||||
|
||||
final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
|
||||
{
|
||||
/**
|
||||
* @var TypeSpecifier
|
||||
*/
|
||||
private $typeSpecifier;
|
||||
|
||||
/**
|
||||
* @var RegexArrayShapeMatcher
|
||||
*/
|
||||
private $regexShapeMatcher;
|
||||
|
||||
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
|
||||
{
|
||||
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||
}
|
||||
|
||||
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
|
||||
{
|
||||
$this->typeSpecifier = $typeSpecifier;
|
||||
}
|
||||
|
||||
public function getClass(): string
|
||||
{
|
||||
return Preg::class;
|
||||
}
|
||||
|
||||
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
|
||||
{
|
||||
return in_array($methodReflection->getName(), [
|
||||
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
|
||||
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
|
||||
], true)
|
||||
&& !$context->null();
|
||||
}
|
||||
|
||||
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
|
||||
{
|
||||
$args = $node->getArgs();
|
||||
$patternArg = $args[0] ?? null;
|
||||
$matchesArg = $args[2] ?? null;
|
||||
$flagsArg = $args[3] ?? null;
|
||||
|
||||
if (
|
||||
$patternArg === null || $matchesArg === null
|
||||
) {
|
||||
return new SpecifiedTypes();
|
||||
}
|
||||
|
||||
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||
if ($flagsType === null) {
|
||||
return new SpecifiedTypes();
|
||||
}
|
||||
|
||||
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
|
||||
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
|
||||
} else {
|
||||
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
|
||||
}
|
||||
|
||||
if ($matchedType === null) {
|
||||
return new SpecifiedTypes();
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)
|
||||
) {
|
||||
$matchedType = PregMatchFlags::removeNullFromMatches($matchedType);
|
||||
}
|
||||
|
||||
$overwrite = false;
|
||||
if ($context->false()) {
|
||||
$overwrite = true;
|
||||
$context = $context->negate();
|
||||
}
|
||||
|
||||
// @phpstan-ignore function.alreadyNarrowedType
|
||||
if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) {
|
||||
$typeSpecifier = $this->typeSpecifier->create(
|
||||
$matchesArg->value,
|
||||
$matchedType,
|
||||
$context,
|
||||
$scope
|
||||
)->setRootExpr($node);
|
||||
|
||||
return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier;
|
||||
}
|
||||
|
||||
// @phpstan-ignore arguments.count
|
||||
return $this->typeSpecifier->create(
|
||||
$matchesArg->value,
|
||||
$matchedType,
|
||||
$context,
|
||||
// @phpstan-ignore argument.type
|
||||
$overwrite,
|
||||
$scope,
|
||||
$node
|
||||
);
|
||||
}
|
||||
}
|
91
vendor/composer/pcre/src/PHPStan/PregReplaceCallbackClosureTypeExtension.php
vendored
Normal file
91
vendor/composer/pcre/src/PHPStan/PregReplaceCallbackClosureTypeExtension.php
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Pcre\PHPStan;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use Composer\Pcre\Regex;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Reflection\MethodReflection;
|
||||
use PHPStan\Reflection\Native\NativeParameterReflection;
|
||||
use PHPStan\Reflection\ParameterReflection;
|
||||
use PHPStan\TrinaryLogic;
|
||||
use PHPStan\Type\ClosureType;
|
||||
use PHPStan\Type\Constant\ConstantArrayType;
|
||||
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
|
||||
use PHPStan\Type\StringType;
|
||||
use PHPStan\Type\TypeCombinator;
|
||||
use PHPStan\Type\Type;
|
||||
|
||||
final class PregReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
|
||||
{
|
||||
/**
|
||||
* @var RegexArrayShapeMatcher
|
||||
*/
|
||||
private $regexShapeMatcher;
|
||||
|
||||
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
|
||||
{
|
||||
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||
}
|
||||
|
||||
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
|
||||
{
|
||||
return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true)
|
||||
&& in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true)
|
||||
&& $parameter->getName() === 'replacement';
|
||||
}
|
||||
|
||||
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
|
||||
{
|
||||
$args = $methodCall->getArgs();
|
||||
$patternArg = $args[0] ?? null;
|
||||
$flagsArg = $args[5] ?? null;
|
||||
|
||||
if (
|
||||
$patternArg === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||
|
||||
$matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
|
||||
if ($matchesType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) {
|
||||
$matchesType = $matchesType->getConstantArrays()[0];
|
||||
$matchesType = new ConstantArrayType(
|
||||
$matchesType->getKeyTypes(),
|
||||
array_map(static function (Type $valueType): Type {
|
||||
if (count($valueType->getConstantArrays()) === 1) {
|
||||
$valueTypeArray = $valueType->getConstantArrays()[0];
|
||||
return new ConstantArrayType(
|
||||
$valueTypeArray->getKeyTypes(),
|
||||
array_map(static function (Type $valueType): Type {
|
||||
return TypeCombinator::removeNull($valueType);
|
||||
}, $valueTypeArray->getValueTypes()),
|
||||
$valueTypeArray->getNextAutoIndexes(),
|
||||
[],
|
||||
$valueTypeArray->isList()
|
||||
);
|
||||
}
|
||||
return TypeCombinator::removeNull($valueType);
|
||||
}, $matchesType->getValueTypes()),
|
||||
$matchesType->getNextAutoIndexes(),
|
||||
[],
|
||||
$matchesType->isList()
|
||||
);
|
||||
}
|
||||
|
||||
return new ClosureType(
|
||||
[
|
||||
new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
|
||||
],
|
||||
new StringType()
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue