resolved conflicts

This commit is contained in:
2025-03-31 03:46:05 +03:00
6454 changed files with 1520539 additions and 9 deletions
@@ -0,0 +1,244 @@
# Africa's Talking PHP SDK
[![Latest Stable Version](https://img.shields.io/packagist/v/africastalking/africastalking)](https://packagist.org/packages/africastalking/africastalking)
> This SDK provides convenient access to the Africa's Talking API for applications written in PHP.
## Documentation
Take a look at the [API docs here](https://developers.africastalking.com).
## Install
You can install the PHP SDK via composer or by downloading the source
#### Via Composer
The recommended way to install the SDK is with [Composer](http://getcomposer.org/).
```bash
composer require africastalking/africastalking
```
## Usage
The SDK needs to be instantiated using your username and API key, which you can get from the [dashboard](https://account.africastalking.com).
> You can use this SDK for either production or sandbox apps. For sandbox, the app username is **ALWAYS** `sandbox`
```php
use AfricasTalking\SDK\AfricasTalking;
$username = 'YOUR_USERNAME'; // use 'sandbox' for development in the test environment
$apiKey = 'YOUR_API_KEY'; // use your sandbox app API key for development in the test environment
$AT = new AfricasTalking($username, $apiKey);
// Get one of the services
$sms = $AT->sms();
// Use the service
$result = $sms->send([
'to' => '+2XXYYYOOO',
'message' => 'Hello World!'
]);
print_r($result);
```
See [example](example/) for more usage examples.
## Instantiation
Instantiating the class will give you an object with available methods
- `$AT = new AfricasTalking($username, $apiKey)`: Instantiate the class
- Get available service
- [SMS Service](#sms): `$sms = $AT->sms()`
- [Content Service](#content): `$content = $AT->content()`
- [Airtime Service](#airtime): `$airtime = $AT->airtime()`
- [Mobile Data Service](#mobiledata): `$mobileData = $AT->mobileData()`
- [Voice Service](#voice): `$voice = $AT->voice()`
- [Token Service](#token): `$token = $AT->token()`
- [Application Service](#application): `$application = $AT->application()`
### Application
- `fetchApplicationData()`: Get app information. e.g balance
### Airtime
- `send($parameters, $options)`: Send airtime
- **$parameters:** associative array with the following keys:
- `recipients`: An array of arrays containing the following keys
- `phoneNumber`: Recipient of airtime. `REQUIRED`
- `currencyCode`: 3-digit ISO format currency code (e.g `KES`, `USD`, `UGX` etc). `REQUIRED`
- `amount`: Amount to send. `REQUIRED`
- **$options:** optional associative array with the following keys:
- `idempotencyKey`: Key to use when making idempotent requests
- `maxNumRetry`: Maximum number of retries in case of failed airtime deliveries due to telco unavailability or any other reason.
### SMS
- `send($options)`: Send a message
- `message`: SMS content. `REQUIRED`
- `to`: An array of phone numbers. `REQUIRED`
- `from`: Shortcode or alphanumeric ID that is registered with your Africa's Talking account.
- `enqueue`: Set to `true` if you would like to deliver as many messages to the API without waiting for an acknowledgement from telcos.
- `fetchMessages($options)`: Fetch your messages
- `lastReceivedId`: This is the id of the message you last processed. Defaults to `0`
***The followoing methods have been moved to the content service, but, have been maintained on SMS for backwards compatibility:***
- `sendPremium($options)`: Send a premium SMS. Calls `$content->send($options)`
- `createSubscription($options)`: Create a premium subscription. Calls `$content->createSubscription($options)`
- `fetchSubscriptions($options)`: Fetch your premium subscription data. Calls `$content->fetchSubscriptions($options)`
- `deleteSubscription($options)`: Delete a phone number from a premium subscription. Calls `$content->$deleteSubscription($options)`
### Content
- `send($options)`: Send a premium SMS
- `message`: SMS content. `REQUIRED`
- `to`: An array of phone numbers. `REQUIRED`
- `from`: Shortcode that is registered with your Africa's Talking account. `REQUIRED`
- `keyword`: Your premium product keyword
- `linkId`: "[...] We forward the `linkId` to your application when a user sends a message to your onDemand service"
- `retryDurationInHours`: "This specifies the number of hours your subscription message should be retried in case it's not delivered to the subscriber"
- `createSubscription($options)`: Create a premium subscription
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and is also mapped to your account. `REQUIRED`
- `phoneNumber`: PhoneNumber to be subscribed `REQUIRED`
- `fetchSubscriptions($options)`: Fetch your premium subscription data
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and mapped to your account. `REQUIRED`
- `lastReceivedId`: ID of the subscription you believe to be your last. Defaults to `0`
- `deleteSubscription($options)`: Delete a phone number from a premium subscription
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and is also mapped to your account. `REQUIRED`
- `phoneNumber`: PhoneNumber to be subscribed `REQUIRED`
### Mobile Data
- `send($parameters, $options)`: Send mobile data to customers
- **$parameters:** associative array with the following keys:
- `productName`: Payment product on Africa's Talking. `REQUIRED`
- `recipients`: A list of recipients. Each recipient has:
- `phoneNumber`: Customer phone number (in international format). `REQUIRED`
- `quantity`: Mobile data amount. `REQUIRED`
- `unit`: Mobile data unit. Can either be `MB` or `GB`. `REQUIRED`
- `validity`: How long the mobile data is valid for. Must be one of `Day`, `Week` and `Month`. `REQUIRED`
- `metadata`: Additional data to associate with the tranasction. `REQUIRED`
- **$options:** optional associative array with the following keys:
- `idempotencyKey`: Key to use when making idempotent requests
- `findTransaction($parameters)`: Find a particular transaction
- `transactionId`: ID of trancation to find. `REQUIRED`
- `fetchWalletBalance()`: Fetch your payment wallet balance
### Voice
- `call($options)`: Initiate a phone call
- `to`: Phone number that you wish to dial (in international format). `REQUIRED`
- `from`: Phone number on Africa's Talking (in international format). `REQUIRED`
- `clientRequestId`: Variable sent to your Events Callback URL that can be used to tag the call. `OPTIONAL`
- `fetchQueuedCalls($options)`: Fetch queued calls on a phone number
- `phoneNumber`: Phone number mapped to your Africa's Talking account (in international format). `REQUIRED`
- `name`: Fetch calls for a specific queue.
- `uploadMediaFile($options)`: Upload a voice media file
- `phoneNumber`: phone number mapped to your Africa's Talking account (in international format). `REQUIRED`
- `url`: The url of the file to upload. Should start with `http(s)://`. `REQUIRED`
#### MessageBuilder
Build voice xml when callback URL receives a POST from the voice API. Actions can be chained to create an XML string.
```php
$voiceActions = $voice->messageBuilder();
$xmlresponse = $voiceActions
->getDigits($options)
->say($text)
->record()
->build();
```
- `say($text)`: Add a `Say` action
- `text`: Text (in English) that will be read out to the user.
- `play($url)`: Add a `Play` action
- `url`: Public url to an audio file. This file will be played back to user.
- `getDigits($options)`: Add a `GetDigits` action
- `numDigits`: Number of digits should be gotten from the user
- `timeout`: Timeout (in seconds) for getting digits from a user.
- `finishOnKey`: key which will terminate the action of getting digits.
- `callbackUrl`: URL to forward the results of the GetDigits action.
- `dial($options)`: Add a `Dial` action
- `phoneNumbers`: An array of phone numbers (in international format) to call. `REQUIRED`
- `record`: Boolean - Whether to record the conversation.
- `sequenntial`: Boolean - If many numbers provided for `phoneNumbers`, determines whether the phone numbers will be dialed one after the other or at the same time.
- `callerId`: Africa's Talking number you want to dial out with.
- `ringBackTone`: URL location of a media playback you would want the user to listen to when the call has been placed before its picked up.
- `maxDuration`: maximum amount of time in seconds a call should take.
- `conference()`: Add a `Conference` action
- `record($options)`: Add a `Record` action
- `finishOnKey`: Key which will terminate the action of recording.
- `maxLength`: Maximum amount of time in seconds a recording should take.
- `timeout`: Timeout (in seconds) for getting a recording from a user.
- `trimSilence`: Boolean - Specifies whether you want to remove the initial and final parts of a recording where user was silent.
- `playBeep`: Boolean - Specifies whether the API should play a beep when recording starts.
- `callbackUrl`: URL to forward the results of the Recording action.
- `enqueue($options)`: Add an `Enqueue` action
- `holdMusic`: URL to the file to be played while the user is on hold.
- `name`: Name of queue to put call on.
- `deqeue($options)`: Add a `Dequeue` acton
- `phoneNumber`: Phone number mapped to your Africa's Talking account which a user called to join the queue. `REQUIRED`
- `name`: Name of queue you want to dequeue from.
- `reject()`: Add a `Reject` action
- `redirect($url)`: Add a `Redirect` action
- `url`: URL to transfer control of the call to
- `build()`: Build the xml after chaining some of the above actions
### Token
- `generateAuthToken()`: Generate an auth token to use for authenticating API requests instead of your API key.
## Testing the SDK
The SDK uses [PHPUnit](https://phpunit.de/manual/current/en/index.html) as the test runner.
To run available tests, from the root of the project run:
```bash
# Configure needed fixtures, e.g sandbox api key, Africa's Talking products
cp tests/Fixtures.php.tpl tests/Fixtures.php
# Run tests
phpunit --testdox
```
## Issues
If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/AfricasTalkingLtd/africastalking-php/issues).
@@ -0,0 +1,32 @@
{
"name": "africastalking/africastalking",
"description": "Official Africa's Talking PHP SDK",
"keywords": ["sms", "voice", "ussd", "text message", "airtime", "api", "africastalking"],
"homepage": "http://github.com/AfricasTalkingLtd/africastalking-php",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Africas's Talking",
"email": "support@africastalking.com",
"homepage": "https://www.africastalking.com"
}
],
"require": {
"php": ">=7.1",
"guzzlehttp/guzzle": "^6.0 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
},
"autoload": {
"psr-4": {
"AfricasTalking\\SDK\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"AfricasTalking\\SDK\\Tests\\": "tests"
}
}
}
@@ -0,0 +1,137 @@
<?php
namespace AfricasTalking\SDK;
use GuzzleHttp\Client;
class AfricasTalking
{
const BASE_DOMAIN = "africastalking.com";
const BASE_SANDBOX_DOMAIN = "sandbox." . self::BASE_DOMAIN;
protected $username;
protected $apiKey;
protected $client;
protected $contentClient;
protected $voiceClient;
protected $tokenClient;
protected $mobileDataClient;
protected $baseDomain;
public $baseUrl;
protected $voiceUrl;
protected $checkoutTokenUrl;
protected $contentUrl;
protected $mobileDataUrl;
public function __construct($username, $apiKey)
{
if($username === 'sandbox') {
$this->baseDomain = self::BASE_SANDBOX_DOMAIN;
} else {
$this->baseDomain = self::BASE_DOMAIN;
}
$this->baseUrl = "https://api." . $this->baseDomain . "/version1/";
$this->voiceUrl = "https://voice." . $this->baseDomain . "/";
$this->mobileDataUrl = "https://bundles." . $this->baseDomain . "/";
$this->contentUrl = ($username === "sandbox") ? ($this->baseUrl) : ("https://content." . $this->baseDomain . "/version1/");
$this->checkoutTokenUrl = "https://api." . $this->baseDomain . "/";
if ($username === 'sandbox') {
$this->contentUrl = $this->baseUrl;
}
$this->username = $username;
$this->apiKey = $apiKey;
$this->client = new Client([
'base_uri' => $this->baseUrl,
'headers' => [
'apikey' => $this->apiKey,
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json'
]
]);
$this->contentClient = new Client([
'base_uri' => $this->contentUrl,
'headers' => [
'apikey' => $this->apiKey,
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json'
]
]);
$this->voiceClient = new Client([
'base_uri' => $this->voiceUrl,
'headers' => [
'apikey' => $this->apiKey,
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json'
]
]);
$this->mobileDataClient = new Client([
'base_uri' => $this->mobileDataUrl,
'headers' => [
'apikey' => $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json'
]
]);
$this->tokenClient = new Client([
'base_uri' => $this->checkoutTokenUrl,
'headers' => [
'apikey' => $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json'
]
]);
}
public function sms()
{
$content = new Content($this->contentClient, $this->username, $this->apiKey);
$sms = new SMS($this->client, $this->username, $this->apiKey, $content);
return $sms;
}
public function content()
{
$content = new Content($this->contentClient, $this->username, $this->apiKey);
return $content;
}
public function airtime()
{
$airtime = new Airtime($this->client, $this->username, $this->apiKey);
return $airtime;
}
public function voice()
{
$voice = new Voice($this->voiceClient, $this->username, $this->apiKey);
return $voice;
}
public function application()
{
$application = new Application($this->client, $this->username, $this->apiKey);
return $application;
}
public function mobileData()
{
$mobileData = new MobileData($this->mobileDataClient, $this->username, $this->apiKey);
return $mobileData;
}
public function token()
{
$token = new Token($this->tokenClient, $this->username, $this->apiKey);
return $token;
}
}
@@ -0,0 +1,132 @@
<?php
namespace AfricasTalking\SDK;
class Content extends Service
{
public function send ($options)
{
if (empty($options['to']) || empty($options['message'])) {
return $this->error('recipient and message must be defined');
}
if (!is_array($options['to'])) {
$options['to'] = [$options['to']];
}
$data = [
'username' => $this->username,
'to' => implode(",", $options['to']),
'message' => $options['message']
];
if (array_key_exists('enqueue', $options) && $options['enqueue']) {
$data['enqueue'] = 1;
}
if (empty($options['from'])) {
return [
'status' => 'error',
'data' => 'from is required for premium SMS'
];
} else {
$data['from'] = $options['from'];
}
if (!empty($options['keyword'])) {
$data['keyword'] = $options['keyword'];
}
if (!empty($options['linkId'])) {
$data['linkId'] = $options['linkId'];
}
if (!empty($options['retryDurationInHours'])) {
$data['retryDurationInHours'] = $options['retryDurationInHours'];
}
// turn off bulk sms mode
$data['bulkSMSMode'] = 0;
$response = $this->client->post('messaging', ['form_params' => $data ]);
return $this->success($response);
}
public function createSubscription ($options)
{
if (empty($options['phoneNumber']) ||
empty($options['shortCode']) ||
empty($options['keyword'])) {
return $this->error("phoneNumber, shortCode and keyword must be specified");
}
$data = [
'username' => $this->username,
'phoneNumber' => $options['phoneNumber'],
'shortCode' => $options['shortCode'],
'keyword' => $options['keyword']
];
/**
* checkoutToken Key was removed in commit:339f7057d8ff640ffa9802b4d3a812848b1072a9.
* To prevent breaking applications in production, we conditionally add it to
* the request otherwise previous behaviour persists.
**/
if(array_key_exists('checkoutToken',$options)){
$data['checkoutToken'] = $options['checkoutToken'];
}
$response = $this->client->post('subscription/create', ['form_params' => $data ] );
return $this->success($response);
}
public function deleteSubscription ($options)
{
if (empty($options['phoneNumber']) ||
empty($options['shortCode']) ||
empty($options['keyword'])) {
return $this->error("phoneNumber, shortCode and keyword must be specified");
}
$data = [
'username' => $this->username,
'phoneNumber' => $options['phoneNumber'],
'shortCode' => $options['shortCode'],
'keyword' => $options['keyword']
];
$response = $this->client->post('subscription/delete', ['form_params' => $data ] );
return $this->success($response);
}
public function fetchSubscriptions($options)
{
if(empty($options['shortCode']) || empty($options['keyword'])) {
return $this->error("shortCode and keyword must be specified");
}
if (empty($options['lastReceivedId'])) {
$options['lastReceivedId'] = 0;
}
if (!is_numeric($options['lastReceivedId'])) {
return $this->error('lastReceivedId must be an integer');
}
$data = [
'username' => $this->username,
'lastReceivedId' => $options['lastReceivedId'],
'shortCode' => $options['shortCode'],
'keyword' => $options['keyword']
];
$response = $this->client->get('subscription', ['query' => $data ] );
return $this->success($response);
}
}
@@ -0,0 +1,104 @@
<?php
namespace AfricasTalking\SDK;
class MobileData extends Service
{
public function __call($method, $args)
{
// First check if method exists
if (method_exists($this, 'do' . $method)) {
$func = 'do' . $method;
if (!isset($args[0])) {
$args = [ 0 => ''];
}
return $this->$func($args[0]);
} else {
return $this->error($method .' is an invalid Mobile Data SDK Method');
}
}
protected function doSend($parameters, $options = [])
{
// Check if productName is set
if (!isset($parameters['productName'])) {
return $this->error('productName must be defined');
}
$productName = $parameters['productName'];
// Check if recipients array is provided
if (!isset($parameters['recipients'])) {
return $this->error('recipients must be an array containing phoneNumber, unit, quatity, validity and metadata');
} else if (isset($parameters['recipients']) && is_array($parameters['recipients'])) {
$recipients = $parameters['recipients'];
foreach ($recipients as $r) {
if (!isset($r['phoneNumber']) ||
!isset($r['quantity']) ||
!isset($r['unit']) ||
!isset($r['validity']) ||
!isset($r['metadata'])) {
return $this->error('recipients must be an array containing phoneNumber, quantity, unit, validity and metadata');
}
if (isset($r['validity'])) {
if (!in_array($r['validity'], ['Day', 'Month', 'Week'])) {
return $this->error('validity must be one of Day, Week, Month');
}
}
if (isset($r['unit'])) {
if (!in_array($r['unit'], ['MB', 'GB'])) {
return $this->error('unit must be one of MB, GB');
}
}
}
}
// Make request data array
$requestData = [
'username' => $this->username,
'productName' => $productName,
'recipients' => $recipients,
];
$requestOptions = [
'json' => $requestData,
];
if(isset($options['idempotencyKey'])) {
$requestOptions['headers'] = [
'Idempotency-Key' => $options['idempotencyKey'],
];
}
$response = $this->client->post('mobile/data/request', $requestOptions);
return $this->success($response);
}
protected function doFindTransaction($options)
{
if (!isset($options['transactionId'])) {
return $this->error('transactionId must be defined');
}
$requestData = [
'username' => $this->username,
'transactionId' => $options['transactionId']
];
$response = $this->client->get('query/transaction/find', ['query' => $requestData]);
return $this->success($response);
}
protected function doFetchWalletBalance()
{
$requestData = [
'username' => $this->username
];
$response = $this->client->get('query/wallet/balance', ['query' => $requestData]);
return $this->success($response);
}
}
@@ -0,0 +1,47 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class AfricasTalkingTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$this->client = new AfricasTalking($this->username, $this->apiKey);
}
public function testSMSClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\SMS::class, $this->client->sms());
}
public function testContentClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Content::class, $this->client->content());
}
public function testAirtimeClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Airtime::class, $this->client->airtime());
}
public function testVoiceClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Voice::class, $this->client->voice());
}
public function testApplicationClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Application::class, $this->client->application());
}
public function testMobileDataClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\MobileData::class, $this->client->mobileData());
}
}
@@ -0,0 +1,64 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class AirtimeTest extends \PHPUnit\Framework\TestCase
{
public function setup(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->airtime();
}
public function testSendAirtimeToOne()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
]]
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
public function testSendAirtimeIdempotency()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
]]
], [
'idempotencyKey' => 'req-' . mt_rand(10, 100),
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
public function testSendAirtimeToMany()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
], [
'phoneNumber' => '+2347038151149',
'currencyCode' => 'NGN',
'amount' => '10000'
]]
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,25 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class ApplicationTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->application();
}
public function testFetchAplication()
{
$response = $this->client->fetchApplicationData();
$this->assertObjectHasProperty('UserData', $response['data']);
}
}
@@ -0,0 +1,67 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class ContentTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->content();
$this->tokenClient = $at->token();
}
public function send()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'linkId' => 'messageLinkId',
'keyword' => Fixtures::$keyword,
'from' => Fixtures::$shortCode,
'message' => 'Testing Premium...'
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testCreateSubscription()
{
$response = $this->client->createSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword,
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testDeleteSubscription()
{
$response = $this->client->deleteSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testFetchSubscriptions()
{
$response = $this->client->fetchSubscriptions([
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,138 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class SMSTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->sms();
$this->tokenClient = $at->token();
}
public function testSMSWithEmptyMessage()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testSMSWithEmptyRecipient()
{
$response = $this->client->send([
'message' => 'Testing...'
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testSingleSMSSending()
{
$response = $this->client->send([
'to' => Fixtures::$phoneNumber,
'message' => 'Testing SMS...'
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testMultipleSMSSending()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'message' => 'Testing multiple sending...'
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testSMSSendingWithShortcode()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'message' => 'Testing with short code...',
'from' => Fixtures::$shortCode
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testSMSSendingWithAlphanumeric()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'message' => 'Testing with AlphaNumeric...',
'from' => Fixtures::$alphanumeric
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testPremiumSMSSending()
{
$response = $this->client->sendPremium([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'linkId' => 'messageLinkId',
'keyword' => Fixtures::$keyword,
'from' => Fixtures::$shortCode,
'message' => 'Testing Premium...'
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testFetchMessages()
{
$response = $this->client->fetchMessages(['lastReceivedId' => '8796']);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testCreateSubscription()
{
$response = $this->client->createSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword,
]);
$this->assertArrayHasKey('status',$response);
$this->assertArrayHasKey('data',$response);
$this->assertEquals('success',$response['status']);
$this->assertEquals('Success',$response['data']->status);
}
public function testDeleteSubscription()
{
$response = $this->client->deleteSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testFetchSubscriptions()
{
$response = $this->client->fetchSubscriptions([
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,25 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class TokenTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->token();
}
public function testGenerateAuthToken()
{
$response = $this->client->generateAuthToken();
$this->assertEquals(3600, $response['data']->lifetimeInSeconds);
}
}
@@ -0,0 +1,91 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
#[\AllowDynamicProperties]
class VoiceTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->voice();
}
public function testCall()
{
$response = $this->client->call([
'from' => Fixtures::$voicePhoneNumber,
'to' => Fixtures::$voicePhoneNumber2
]);
$this->assertObjectHasProperty('entries', $response['data']);
}
public function testCallsMustHaveRequiredAttributes()
{
$response = $this->client->call([
'from' => Fixtures::$voicePhoneNumber
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testFetchQueuedCalls()
{
$response = $this->client->fetchQueuedCalls([
'phoneNumber' => Fixtures::$voicePhoneNumber,
'name' => 'someQueueName'
]);
$this->assertArrayHasKey('status', $response);
}
public function testFetchQueuedCallsMustHaveRequiredAttributes()
{
$response = $this->client->fetchQueuedCalls();
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testUploadMediaFile()
{
$response = $this->client->uploadMediaFile([
'phoneNumber' => Fixtures::$voicePhoneNumber,
'url' => Fixtures::$mediaUrl
]);
$this->assertArrayHasKey('status', $response);
}
public function testuploadMediaFileMustHaveRequiredAttributes()
{
$response = $this->client->uploadMediaFile([
'url' => 'test@google'
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testuploadMediaFileCannotBeEmpty()
{
$response = $this->client->uploadMediaFile();
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
// public function testMessageBuilder()
// {
// // TODO
// }
}
+25
View File
@@ -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 ComposerAutoloaderInit69a41de4ce3c76c7865a7de0eec12ccc::getLoader();
@@ -0,0 +1,57 @@
{
"name": "barryvdh/laravel-snappy",
"description": "Snappy PDF/Image for Laravel",
"keywords": [
"laravel",
"snappy",
"pdf",
"image",
"wkhtmltopdf",
"wkhtmltoimage"
],
"license": "MIT",
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"require": {
"php": ">=7.2",
"illuminate/support": "^9|^10|^11.0",
"illuminate/filesystem": "^9|^10|^11.0",
"knplabs/knp-snappy": "^1.4.4"
},
"require-dev": {
"orchestra/testbench": "^7|^8|^9.0"
},
"autoload": {
"psr-4": {
"Barryvdh\\Snappy\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Barryvdh\\Snappy\\Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
},
"laravel": {
"providers": [
"Barryvdh\\Snappy\\ServiceProvider"
],
"aliases": {
"PDF": "Barryvdh\\Snappy\\Facades\\SnappyPdf",
"SnappyImage": "Barryvdh\\Snappy\\Facades\\SnappyImage"
}
}
},
"scripts": {
"test": "phpunit"
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,52 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Snappy PDF / Image Configuration
|--------------------------------------------------------------------------
|
| This option contains settings for PDF generation.
|
| Enabled:
|
| Whether to load PDF / Image generation.
|
| Binary:
|
| The file path of the wkhtmltopdf / wkhtmltoimage executable.
|
| Timeout:
|
| The amount of time to wait (in seconds) before PDF / Image generation is stopped.
| Setting this to false disables the timeout (unlimited processing time).
|
| Options:
|
| The wkhtmltopdf command options. These are passed directly to wkhtmltopdf.
| See https://wkhtmltopdf.org/usage/wkhtmltopdf.txt for all options.
|
| Env:
|
| The environment variables to set while running the wkhtmltopdf process.
|
*/
'pdf' => [
'enabled' => true,
'binary' => env('WKHTML_PDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
'timeout' => false,
'options' => [],
'env' => [],
],
'image' => [
'enabled' => true,
'binary' => env('WKHTML_IMG_BINARY', '/usr/local/bin/wkhtmltoimage'),
'timeout' => false,
'options' => [],
'env' => [],
],
];
@@ -0,0 +1,108 @@
<?php namespace Barryvdh\Snappy;
use Knp\Snappy\Image;
use Illuminate\Filesystem\Filesystem;
class IlluminateSnappyImage extends Image {
/**
* @var \Illuminate\Filesystem\Filesystem
*/
protected $fs;
/**
* @param \Illuminate\Filesystem\Filesystem
* @param string $binary
* @param array $options
*/
public function __construct(Filesystem $fs, $binary, array $options, array $env)
{
parent::__construct($binary, $options, $env);
$this->fs = $fs;
}
/**
* Wrapper for the "file_get_contents" function
*
* @param string $filename
*
* @return string
*/
protected function getFileContents($filename)
{
return $this->fs->get($filename);
}
/**
* Wrapper for the "file_exists" function
*
* @param string $filename
*
* @return boolean
*/
protected function fileExists($filename)
{
return $this->fs->exists($filename);
}
/**
* Wrapper for the "is_file" method
*
* @param string $filename
*
* @return boolean
*/
protected function isFile($filename)
{
return $this->fs->isFile($filename);
}
/**
* Wrapper for the "filesize" function
*
* @param string $filename
*
* @return integer or FALSE on failure
*/
protected function filesize($filename)
{
return $this->fs->size($filename);
}
/**
* Wrapper for the "unlink" function
*
* @param string $filename
*
* @return boolean
*/
protected function unlink($filename)
{
return $this->fs->delete($filename);
}
/**
* Wrapper for the "is_dir" function
*
* @param string $filename
*
* @return boolean
*/
protected function isDir($filename)
{
return $this->fs->isDirectory($filename);
}
/**
* Wrapper for the mkdir function
*
* @param string $pathname
*
* @return boolean
*/
protected function mkdir($pathname)
{
return $this->fs->makeDirectory($pathname, 0777, true, true);
}
}
@@ -0,0 +1,208 @@
<?php namespace Barryvdh\Snappy;
use Illuminate\Http\Response;
use Knp\Snappy\Image as SnappyImage;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* A Laravel wrapper for SnappyImage
*
* @package laravel-snappy
* @author Killian Blais
*/
class ImageWrapper {
/**
* @var \Knp\Snappy\Image
*/
protected $snappy;
/**
* @var array
*/
protected $options = array();
/**
* @var string
*/
protected $html;
/**
* @var string
*/
protected $file;
/**
* @param \Knp\Snappy\Image $snappy
*/
public function __construct(SnappyImage $snappy)
{
$this->snappy = $snappy;
}
/**
* Get the Snappy instance.
*
* @return \Knp\Snappy\Image
*/
public function snappy()
{
return $this->snappy;
}
public function setOption($name, $value)
{
$this->snappy->setOption($name, $value);
return $this;
}
public function setOptions($options)
{
$this->snappy->setOptions($options);
return $this;
}
/**
* Load a HTML string
*
* @param string $string
* @return static
*/
public function loadHTML($string)
{
$this->html = (string) $string;
$this->file = null;
return $this;
}
/**
* Load a HTML file
*
* @param string $file
* @return static
*/
public function loadFile($file)
{
$this->html = null;
$this->file = $file;
return $this;
}
public function loadView($view, $data = array(), $mergeData = array())
{
$this->html = View::make($view, $data, $mergeData)->render();
$this->file = null;
return $this;
}
/**
* Output the PDF as a string.
*
* @return string The rendered PDF as string
* @throws \InvalidArgumentException
*/
public function output()
{
if ($this->html)
{
return $this->snappy->getOutputFromHtml($this->html, $this->options);
}
if ($this->file)
{
return $this->snappy->getOutput($this->file, $this->options);
}
throw new \InvalidArgumentException('Image Generator requires a html or file in order to produce output.');
}
/**
* Save the image to a file
*
* @param $filename
* @return static
*/
public function save($filename, $overwrite = false)
{
if ($this->html)
{
$this->snappy->generateFromHtml($this->html, $filename, $this->options, $overwrite);
}
elseif ($this->file)
{
$this->snappy->generate($this->file, $filename, $this->options, $overwrite);
}
return $this;
}
/**
* Make the image downloadable by the user
*
* @param string $filename
* @return \Symfony\Component\HttpFoundation\Response
*/
public function download($filename = 'image.jpg')
{
return new Response($this->output(), 200, array(
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'attachment; filename="'.$filename.'"'
));
}
/**
* Return a response with the image to show in the browser
*
* @param string $filename
* @return \Illuminate\Http\Response
*/
public function inline($filename = 'image.jpg')
{
return new Response($this->output(), 200, array(
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
));
}
/**
* Return a response with the image to show in the browser
*
* @deprecated Use inline() instead
* @param string $filename
* @return \Symfony\Component\HttpFoundation\Response
*/
public function stream($filename = 'image.jpg')
{
return new StreamedResponse(function() {
echo $this->output();
}, 200, array(
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
));
}
/**
* Call Snappy instance.
*
* Also shortcut's
* ->html => loadHtml
* ->view => loadView
* ->file => loadFile
*
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call($name, array $arguments)
{
$method = 'load' . ucfirst($name);
if (method_exists($this, $method))
{
return call_user_func_array(array($this, $method), $arguments);
}
return call_user_func_array (array($this->snappy, $name), $arguments);
}
}
+37
View File
@@ -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 '../h4cc/wkhtmltoimage-amd64/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}/wkhtmltoimage-amd64" "$@"
return
fi
fi
exec "${dir}/wkhtmltoimage-amd64" "$@"
+37
View File
@@ -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 '../h4cc/wkhtmltopdf-amd64/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}/wkhtmltopdf-amd64" "$@"
return
fi
fi
exec "${dir}/wkhtmltopdf-amd64" "$@"
+463
View File
@@ -0,0 +1,463 @@
# Changelog
All notable changes to this project will be documented in this file.
## [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.
+39
View File
@@ -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": "5.16.0"
},
"autoload": {
"psr-4": {
"Brick\\Math\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Brick\\Math\\Tests\\": "tests/"
}
}
}
@@ -0,0 +1,754 @@
<?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;
/**
* 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
*/
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);
}
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);
}
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 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);
}
public function toBigInteger() : BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
public function toBigDecimal() : BigDecimal
{
return $this;
}
public function toBigRational() : BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
return (float) (string) $this;
}
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,509 @@
<?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;
/**
* 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.
*
* @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);
}
/**
* @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 MathException 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;
final public function jsonSerialize() : string
{
return $this->__toString();
}
}
@@ -0,0 +1,413 @@
<?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;
/**
* 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
*/
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);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
return $this->minus($that)->getSign();
}
public function getSign() : int
{
return $this->numerator->getSign();
}
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;
}
public function toBigDecimal() : BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
}
public function toBigRational() : BigRational
{
return $this;
}
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
$simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
}
public function __toString() : string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
if ($denominator === '1') {
return $numerator;
}
return $this->numerator . '/' . $this->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,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,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,65 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the bcmath library.
*
* @internal
*
* @psalm-immutable
*/
class BcMathCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \bcadd($a, $b, 0);
}
public function sub(string $a, string $b) : string
{
return \bcsub($a, $b, 0);
}
public function mul(string $a, string $b) : string
{
return \bcmul($a, $b, 0);
}
public function divQ(string $a, string $b) : string
{
return \bcdiv($a, $b, 0);
}
public function divR(string $a, string $b) : string
{
return \bcmod($a, $b, 0);
}
public function divQR(string $a, string $b) : array
{
$q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
return [$q, $r];
}
public function pow(string $a, int $e) : string
{
return \bcpow($a, (string) $e, 0);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \bcpowmod($base, $exp, $mod, 0);
}
public function sqrt(string $n) : string
{
return \bcsqrt($n, 0);
}
}
@@ -0,0 +1,572 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* 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.')
};
}
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;
}
public function sub(string $a, string $b) : string
{
return $this->add($a, $this->neg($b));
}
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;
}
public function divQ(string $a, string $b) : string
{
return $this->divQR($a, $b)[0];
}
public function divR(string $a, string $b): string
{
return $this->divQR($a, $b)[1];
}
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];
}
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/
*/
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
*/
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
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;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
<?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',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'9b38cf48e83f5d8f60375221cd213eee' => $vendorDir . '/phpstan/phpstan/bootstrap.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'e23faeee409e941dc9b4c80386209c39' => $vendorDir . '/laracasts/flash/src/Laracasts/Flash/functions.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',
'17d016dc52a631c1e74d2eb8fdd57342' => $vendorDir . '/laravel/helpers/src/helpers.php',
'f18cc91337d49233e5754e93f3ed9ec3' => $vendorDir . '/laravelcollective/html/src/helpers.php',
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
'9f394da3192a168c4633675768d80428' => $vendorDir . '/nwidart/laravel-modules/src/helpers.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'377b22b161c09ed6e5152de788ca020a' => $vendorDir . '/spatie/laravel-permission/src/helpers.php',
'646961a8eab48144f6c03fc7c3185753' => $baseDir . '/app/Http/Helpers/Functions.php',
'e3d6ff15e3a00433920bff18fbed7a52' => $baseDir . '/app/Http/Helpers/Finance.php',
);
+108
View File
@@ -0,0 +1,108 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
'h4cc\\WKHTMLToPDF\\' => array($vendorDir . '/h4cc/wkhtmltopdf-amd64'),
'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\\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 . '/phenx/php-svg-lib/src/Svg'),
'Streamline\\' => array($baseDir . '/app'),
'Spatie\\Permission\\' => array($vendorDir . '/spatie/laravel-permission/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'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'PhpMyAdmin\\SqlParser\\' => array($vendorDir . '/phpmyadmin/sql-parser/src'),
'OwenIt\\Auditing\\' => array($vendorDir . '/owen-it/laravel-auditing/src'),
'Nwidart\\Modules\\' => array($vendorDir . '/nwidart/laravel-modules/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Modules\\' => array($baseDir . '/Modules'),
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
'Masterminds\\' => array($vendorDir . '/masterminds/html5/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\\Ui\\' => array($vendorDir . '/laravel/ui/src'),
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
'Larastan\\Larastan\\' => array($vendorDir . '/larastan/larastan/src'),
'Knp\\Snappy\\' => array($vendorDir . '/knplabs/knp-snappy/src/Knp/Snappy'),
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable'),
'Illuminate\\Foundation\\Auth\\' => array($vendorDir . '/laravel/ui/auth-backend'),
'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'),
'Fx3costa\\LaravelChartJs\\' => array($vendorDir . '/fx3costa/laravelchartjs/src'),
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/php-cors/src'),
'FontLib\\' => array($vendorDir . '/phenx/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\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
'Doctrine\\Common\\Cache\\' => array($vendorDir . '/doctrine/cache/lib/Doctrine/Common/Cache'),
'Doctrine\\Common\\' => array($vendorDir . '/doctrine/event-manager/src'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
'Collective\\Html\\' => array($vendorDir . '/laravelcollective/html/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\\Snappy\\' => array($vendorDir . '/barryvdh/laravel-snappy/src'),
'Barryvdh\\DomPDF\\' => array($vendorDir . '/barryvdh/laravel-dompdf/src'),
'AfricasTalking\\SDK\\' => array($vendorDir . '/africastalking/africastalking/src'),
);
+50
View File
@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit69a41de4ce3c76c7865a7de0eec12ccc
{
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('ComposerAutoloaderInit69a41de4ce3c76c7865a7de0eec12ccc', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit69a41de4ce3c76c7865a7de0eec12ccc', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit69a41de4ce3c76c7865a7de0eec12ccc::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit69a41de4ce3c76c7865a7de0eec12ccc::$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,74 @@
# 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).
## [Unreleased]
## [3.0.3] - 2024-07-08
### Fixed
- Fixed PHP 8.4 deprecation notices (#47)
## [3.0.2] - 2022-10-27
### Fixed
- Added missing return types to docblocks (#44, #45)
## [3.0.1] - 2021-08-13
### Added
- Adds ReturnTypeWillChange to suppress PHP 8.1 warnings (#40)
## [3.0.0] - 2021-01-01
### Added
- Added support for both `.` and `/`-delimited key paths (#24)
- Added parameter and return types to everything; enabled strict type checks (#18)
- Added new exception classes to better identify certain types of errors (#20)
- `Data` now implements `ArrayAccess` (#17)
- Added ability to merge non-associative array values (#31, #32)
### Changed
- All thrown exceptions are now instances or subclasses of `DataException` (#20)
- Calling `get()` on a missing key path without providing a default will throw a `MissingPathException` instead of returning `null` (#29)
- Bumped supported PHP versions to 7.1 - 8.x (#18)
### Fixed
- Fixed incorrect merging of array values into string values (#32)
- Fixed `get()` method behaving as if keys with `null` values didn't exist
## [2.0.0] - 2017-12-21
### Changed
- Bumped supported PHP versions to 7.0 - 7.4 (#12)
- Switched to PSR-4 autoloading
## [1.1.0] - 2017-01-20
### Added
- Added new `has()` method to check for the existence of the given key (#4, #7)
## [1.0.1] - 2015-08-12
### Added
- Added new optional `$default` parameter to the `get()` method (#2)
## [1.0.0] - 2012-07-17
**Initial release!**
[Unreleased]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.3...main
[3.0.3]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.2...v3.0.3
[3.0.2]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.1...v3.0.2
[3.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/dflydev/dflydev-dot-access-data/releases/tag/v1.0.0
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
use Throwable;
/**
* Thrown when trying to access a path that does not exist
*/
class MissingPathException extends DataException
{
/** @var string */
protected $path;
public function __construct(string $path, string $message = '', int $code = 0, ?Throwable $previous = null)
{
$this->path = $path;
parent::__construct($message, $code, $previous);
}
public function getPath(): string
{
return $this->path;
}
}
+50
View File
@@ -0,0 +1,50 @@
# Doctrine DBAL
| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.9][3.9] |
|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|
| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.9 image]][GA 3.9] |
| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] |
| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] |
| N/A | N/A | [![Type Coverage][TypeCov image]][TypeCov] | N/A |
Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features for database schema introspection and schema management.
## More resources:
* [Website](http://www.doctrine-project.org/projects/dbal.html)
* [Documentation](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/)
* [Issue Tracker](https://github.com/doctrine/dbal/issues)
[Coverage 5.0 image]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x/graph/badge.svg
[5.0]: https://github.com/doctrine/dbal/tree/5.0.x
[CodeCov 5.0]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/5.0.x?svg=true
[GA 5.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A5.0.x
[GA 5.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=5.0.x
[Coverage 4.3 image]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x/graph/badge.svg
[4.3]: https://github.com/doctrine/dbal/tree/4.3.x
[CodeCov 4.3]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.3.x?svg=true
[GA 4.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.3.x
[GA 4.3 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.3.x
[Coverage 4.2 image]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x/graph/badge.svg
[4.2]: https://github.com/doctrine/dbal/tree/4.2.x
[CodeCov 4.2]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.2.x?svg=true
[GA 4.2]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.2.x
[GA 4.2 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.2.x
[TypeCov]: https://shepherd.dev/github/doctrine/dbal
[TypeCov image]: https://shepherd.dev/github/doctrine/dbal/coverage.svg
[Coverage 3.9 image]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x/graph/badge.svg
[3.9]: https://github.com/doctrine/dbal/tree/3.9.x
[CodeCov 3.9]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.9.x?svg=true
[GA 3.9]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.9.x
[GA 3.9 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.9.x
@@ -0,0 +1,73 @@
{
"name": "doctrine/dbal",
"type": "library",
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"keywords": [
"abstraction",
"database",
"dbal",
"db2",
"mariadb",
"mssql",
"mysql",
"pgsql",
"postgresql",
"oci8",
"oracle",
"pdo",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"license": "MIT",
"authors": [
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"}
],
"require": {
"php": "^7.4 || ^8.0",
"composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1|^2",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "1.12.6",
"phpstan/phpstan-strict-rules": "^1.6",
"phpunit/phpunit": "9.6.20",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0",
"vimeo/psalm": "4.30.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": ["bin/doctrine-dbal"],
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true
}
},
"autoload": {
"psr-4": { "Doctrine\\DBAL\\": "src" }
},
"autoload-dev": {
"psr-4": { "Doctrine\\DBAL\\Tests\\": "tests" }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\MySQL;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/**
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
*/
public function convert(Exception $exception, ?Query $query): DriverException
{
switch ($exception->getCode()) {
case 1008:
return new DatabaseDoesNotExist($exception, $query);
case 1213:
return new DeadlockException($exception, $query);
case 1205:
return new LockWaitTimeoutException($exception, $query);
case 1050:
return new TableExistsException($exception, $query);
case 1051:
case 1146:
return new TableNotFoundException($exception, $query);
case 1216:
case 1217:
case 1451:
case 1452:
case 1701:
return new ForeignKeyConstraintViolationException($exception, $query);
case 1062:
case 1557:
case 1569:
case 1586:
return new UniqueConstraintViolationException($exception, $query);
case 1054:
case 1166:
case 1611:
return new InvalidFieldNameException($exception, $query);
case 1052:
case 1060:
case 1110:
return new NonUniqueFieldNameException($exception, $query);
case 1064:
case 1149:
case 1287:
case 1341:
case 1342:
case 1343:
case 1344:
case 1382:
case 1479:
case 1541:
case 1554:
case 1626:
return new SyntaxErrorException($exception, $query);
case 1044:
case 1045:
case 1046:
case 1049:
case 1095:
case 1142:
case 1143:
case 1227:
case 1370:
case 1429:
case 2002:
case 2005:
case 2054:
return new ConnectionException($exception, $query);
case 2006:
case 4031:
return new ConnectionLost($exception, $query);
case 1048:
case 1121:
case 1138:
case 1171:
case 1252:
case 1263:
case 1364:
case 1566:
return new NotNullConstraintViolationException($exception, $query);
}
return new DriverException($exception, $query);
}
}
@@ -0,0 +1,100 @@
<?php
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\IBMDB2\ExceptionConverter;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\DB2111Platform;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Schema\DB2SchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;
use Doctrine\Deprecations\Deprecation;
use function assert;
use function preg_match;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for IBM DB2 based drivers.
*/
abstract class AbstractDB2Driver implements VersionAwarePlatformDriver
{
/**
* {@inheritDoc}
*/
public function getDatabasePlatform()
{
return new DB2Platform();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@link DB2Platform::createSchemaManager()} instead.
*/
public function getSchemaManager(Connection $conn, AbstractPlatform $platform)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5458',
'AbstractDB2Driver::getSchemaManager() is deprecated.'
. ' Use DB2Platform::createSchemaManager() instead.',
);
assert($platform instanceof DB2Platform);
return new DB2SchemaManager($conn, $platform);
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
/**
* {@inheritDoc}
*/
public function createDatabasePlatformForVersion($version)
{
if (version_compare($this->getVersionNumber($version), '11.1', '>=')) {
return new DB2111Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5156',
'IBM DB2 < 11.1 support is deprecated and will be removed in DBAL 4.'
. ' Consider upgrading to IBM DB2 11.1 or later.',
);
return $this->getDatabasePlatform();
}
/**
* Detects IBM DB2 server version
*
* @param string $versionString Version string as returned by IBM DB2 server, i.e. 'DB2/LINUXX8664 11.5.8.0'
*
* @throws DBALException
*/
private function getVersionNumber(string $versionString): string
{
if (
preg_match(
'/^(?:[^\s]+\s)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
$versionString,
$versionParts,
) !== 1
) {
throw DBALException::invalidPlatformVersionSpecified(
$versionString,
'^(?:[^\s]+\s)?<major_version>.<minor_version>.<patch_version>',
);
}
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
}
@@ -0,0 +1,231 @@
<?php
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\API\MySQL;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDb1010Platform;
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MariaDb1043Platform;
use Doctrine\DBAL\Platforms\MariaDb1052Platform;
use Doctrine\DBAL\Platforms\MariaDb1060Platform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQL84Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\MySQLSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;
use Doctrine\Deprecations\Deprecation;
use function assert;
use function preg_match;
use function stripos;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for MySQL based drivers.
*/
abstract class AbstractMySQLDriver implements VersionAwarePlatformDriver
{
/**
* {@inheritDoc}
*
* @throws Exception
*/
public function createDatabasePlatformForVersion($version)
{
$mariadb = stripos($version, 'mariadb') !== false;
if ($mariadb) {
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
if (version_compare($mariaDbVersion, '10.10.0', '>=')) {
return new MariaDb1010Platform();
}
if (version_compare($mariaDbVersion, '10.6.0', '>=')) {
return new MariaDb1060Platform();
}
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
return new MariaDb1052Platform();
}
if (version_compare($mariaDbVersion, '10.4.3', '>=')) {
return new MariaDb1043Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6110',
'Support for MariaDB < 10.4 is deprecated and will be removed in DBAL 4.'
. ' Consider upgrading to a more recent version of MariaDB.',
);
if (version_compare($mariaDbVersion, '10.2.7', '>=')) {
return new MariaDb1027Platform();
}
} else {
$oracleMysqlVersion = $this->getOracleMysqlVersionNumber($version);
if (version_compare($oracleMysqlVersion, '8.4.0', '>=')) {
if (! version_compare($version, '8.4.0', '>=')) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/dbal/pull/5779',
'Version detection logic for MySQL will change in DBAL 4. '
. 'Please specify the version as the server reports it, e.g. "8.4.0" instead of "8.4".',
);
}
return new MySQL84Platform();
}
if (version_compare($oracleMysqlVersion, '8', '>=')) {
if (! version_compare($version, '8.0.0', '>=')) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/dbal/pull/5779',
'Version detection logic for MySQL will change in DBAL 4. '
. 'Please specify the version as the server reports it, e.g. "8.0.31" instead of "8".',
);
}
return new MySQL80Platform();
}
if (version_compare($oracleMysqlVersion, '5.7.9', '>=')) {
if (! version_compare($version, '5.7.9', '>=')) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/dbal/pull/5779',
'Version detection logic for MySQL will change in DBAL 4. '
. 'Please specify the version as the server reports it, e.g. "5.7.40" instead of "5.7".',
);
}
return new MySQL57Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5072',
'MySQL 5.6 support is deprecated and will be removed in DBAL 4.'
. ' Consider upgrading to MySQL 5.7 or later.',
);
}
return $this->getDatabasePlatform();
}
/**
* Get a normalized 'version number' from the server string
* returned by Oracle MySQL servers.
*
* @param string $versionString Version string returned by the driver, i.e. '5.7.10'
*
* @throws Exception
*/
private function getOracleMysqlVersionNumber(string $versionString): string
{
if (
preg_match(
'/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/',
$versionString,
$versionParts,
) !== 1
) {
throw Exception::invalidPlatformVersionSpecified(
$versionString,
'<major_version>.<minor_version>.<patch_version>',
);
}
$majorVersion = $versionParts['major'];
$minorVersion = $versionParts['minor'] ?? 0;
$patchVersion = $versionParts['patch'] ?? null;
if ($majorVersion === '5' && $minorVersion === '7') {
$patchVersion ??= '9';
} else {
$patchVersion ??= '0';
}
return $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
}
/**
* Detect MariaDB server version, including hack for some mariadb distributions
* that starts with the prefix '5.5.5-'
*
* @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
*
* @throws Exception
*/
private function getMariaDbMysqlVersionNumber(string $versionString): string
{
if (stripos($versionString, 'MariaDB') === 0) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/dbal/pull/5779',
'Version detection logic for MySQL will change in DBAL 4. '
. 'Please specify the version as the server reports it, '
. 'e.g. "10.9.3-MariaDB" instead of "mariadb-10.9".',
);
}
if (
preg_match(
'/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
$versionString,
$versionParts,
) !== 1
) {
throw Exception::invalidPlatformVersionSpecified(
$versionString,
'^(?:5\.5\.5-)?(mariadb-)?<major_version>.<minor_version>.<patch_version>',
);
}
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
/**
* {@inheritDoc}
*
* @return AbstractMySQLPlatform
*/
public function getDatabasePlatform()
{
return new MySQLPlatform();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@link AbstractMySQLPlatform::createSchemaManager()} instead.
*
* @return MySQLSchemaManager
*/
public function getSchemaManager(Connection $conn, AbstractPlatform $platform)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5458',
'AbstractMySQLDriver::getSchemaManager() is deprecated.'
. ' Use MySQLPlatform::createSchemaManager() instead.',
);
assert($platform instanceof AbstractMySQLPlatform);
return new MySQLSchemaManager($conn, $platform);
}
public function getExceptionConverter(): ExceptionConverter
{
return new MySQL\ExceptionConverter();
}
}
@@ -0,0 +1,93 @@
<?php
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\API\PostgreSQL;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;
use Doctrine\DBAL\Platforms\PostgreSQL120Platform;
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\PostgreSQLSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;
use Doctrine\Deprecations\Deprecation;
use function assert;
use function preg_match;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for PostgreSQL based drivers.
*/
abstract class AbstractPostgreSQLDriver implements VersionAwarePlatformDriver
{
/**
* {@inheritDoc}
*/
public function createDatabasePlatformForVersion($version)
{
if (preg_match('/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/', $version, $versionParts) !== 1) {
throw Exception::invalidPlatformVersionSpecified(
$version,
'<major_version>.<minor_version>.<patch_version>',
);
}
$majorVersion = $versionParts['major'];
$minorVersion = $versionParts['minor'] ?? 0;
$patchVersion = $versionParts['patch'] ?? 0;
$version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
if (version_compare($version, '12.0', '>=')) {
return new PostgreSQL120Platform();
}
if (version_compare($version, '10.0', '>=')) {
return new PostgreSQL100Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5060',
'PostgreSQL 9 support is deprecated and will be removed in DBAL 4.'
. ' Consider upgrading to Postgres 10 or later.',
);
return new PostgreSQL94Platform();
}
/**
* {@inheritDoc}
*/
public function getDatabasePlatform()
{
return new PostgreSQL94Platform();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@link PostgreSQLPlatform::createSchemaManager()} instead.
*/
public function getSchemaManager(Connection $conn, AbstractPlatform $platform)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5458',
'AbstractPostgreSQLDriver::getSchemaManager() is deprecated.'
. ' Use PostgreSQLPlatform::createSchemaManager() instead.',
);
assert($platform instanceof PostgreSQLPlatform);
return new PostgreSQLSchemaManager($conn, $platform);
}
public function getExceptionConverter(): ExceptionConverter
{
return new PostgreSQL\ExceptionConverter();
}
}
@@ -0,0 +1,38 @@
<?php
namespace Doctrine\DBAL\Driver\OCI8\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use SensitiveParameter;
class InitializeSession implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new class ($driver) extends AbstractDriverMiddleware {
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params
): Connection {
$connection = parent::connect($params);
$connection->exec(
'ALTER SESSION SET'
. " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
. " NLS_TIME_FORMAT = 'HH24:MI:SS'"
. " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
. " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM'"
. " NLS_NUMERIC_CHARACTERS = '.,'",
);
return $connection;
}
};
}
}
@@ -0,0 +1,135 @@
<?php
namespace Doctrine\DBAL\Driver\PDO\PgSQL;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\Deprecations\Deprecation;
use PDO;
use PDOException;
use SensitiveParameter;
final class Driver extends AbstractPostgreSQLDriver
{
/**
* {@inheritDoc}
*
* @return Connection
*/
public function connect(
#[SensitiveParameter]
array $params
) {
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
$safeParams = $params;
unset($safeParams['password'], $safeParams['url']);
try {
$pdo = new PDO(
$this->constructPdoDsn($safeParams),
$params['user'] ?? '',
$params['password'] ?? '',
$driverOptions,
);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
if (
! isset($driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES])
|| $driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES] === true
) {
$pdo->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true);
}
$connection = new Connection($pdo);
/* defining client_encoding via SET NAMES to avoid inconsistent DSN support
* - passing client_encoding via the 'options' param breaks pgbouncer support
*/
if (isset($params['charset'])) {
$connection->exec('SET NAMES \'' . $params['charset'] . '\'');
}
return $connection;
}
/**
* Constructs the Postgres PDO DSN.
*
* @param array<string, mixed> $params
*/
private function constructPdoDsn(array $params): string
{
$dsn = 'pgsql:';
if (isset($params['host']) && $params['host'] !== '') {
$dsn .= 'host=' . $params['host'] . ';';
}
if (isset($params['port']) && $params['port'] !== '') {
$dsn .= 'port=' . $params['port'] . ';';
}
if (isset($params['dbname'])) {
$dsn .= 'dbname=' . $params['dbname'] . ';';
} elseif (isset($params['default_dbname'])) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5705',
'The "default_dbname" connection parameter is deprecated. Use "dbname" instead.',
);
$dsn .= 'dbname=' . $params['default_dbname'] . ';';
} else {
if (isset($params['user']) && $params['user'] !== 'postgres') {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5705',
'Relying on the DBAL connecting to the "postgres" database by default is deprecated.'
. ' Unless you want to have the server determine the default database for the connection,'
. ' specify the database name explicitly.',
);
}
// Used for temporary connections to allow operations like dropping the database currently connected to.
$dsn .= 'dbname=postgres;';
}
if (isset($params['sslmode'])) {
$dsn .= 'sslmode=' . $params['sslmode'] . ';';
}
if (isset($params['sslrootcert'])) {
$dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';';
}
if (isset($params['sslcert'])) {
$dsn .= 'sslcert=' . $params['sslcert'] . ';';
}
if (isset($params['sslkey'])) {
$dsn .= 'sslkey=' . $params['sslkey'] . ';';
}
if (isset($params['sslcrl'])) {
$dsn .= 'sslcrl=' . $params['sslcrl'] . ';';
}
if (isset($params['application_name'])) {
$dsn .= 'application_name=' . $params['application_name'] . ';';
}
if (isset($params['gssencmode'])) {
$dsn .= 'gssencmode=' . $params['gssencmode'] . ';';
}
return $dsn;
}
}
@@ -0,0 +1,86 @@
<?php
namespace Doctrine\DBAL\Driver\PgSQL;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use ErrorException;
use SensitiveParameter;
use function addslashes;
use function array_filter;
use function array_keys;
use function array_map;
use function array_slice;
use function array_values;
use function func_get_args;
use function implode;
use function pg_connect;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use const PGSQL_CONNECT_FORCE_NEW;
final class Driver extends AbstractPostgreSQLDriver
{
/** {@inheritDoc} */
public function connect(
#[SensitiveParameter]
array $params
): Connection {
set_error_handler(
static function (int $severity, string $message) {
throw new ErrorException($message, 0, $severity, ...array_slice(func_get_args(), 2, 2));
},
);
try {
$connection = pg_connect($this->constructConnectionString($params), PGSQL_CONNECT_FORCE_NEW);
} catch (ErrorException $e) {
throw new Exception($e->getMessage(), '08006', 0, $e);
} finally {
restore_error_handler();
}
if ($connection === false) {
throw new Exception('Unable to connect to Postgres server.');
}
$driverConnection = new Connection($connection);
if (isset($params['application_name'])) {
$driverConnection->exec('SET application_name = ' . $driverConnection->quote($params['application_name']));
}
return $driverConnection;
}
/**
* Constructs the Postgres connection string
*
* @param array<string, mixed> $params
*/
private function constructConnectionString(
#[SensitiveParameter]
array $params
): string {
$components = array_filter(
[
'host' => $params['host'] ?? null,
'port' => $params['port'] ?? null,
'dbname' => $params['dbname'] ?? 'postgres',
'user' => $params['user'] ?? null,
'password' => $params['password'] ?? null,
'sslmode' => $params['sslmode'] ?? null,
'gssencmode' => $params['gssencmode'] ?? null,
],
static fn ($value) => $value !== '' && $value !== null,
);
return implode(' ', array_map(
static fn ($value, string $key) => sprintf("%s='%s'", $key, addslashes($value)),
array_values($components),
array_keys($components),
));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
<?php
namespace Doctrine\DBAL\Platforms;
/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.2 database platform.
*
* @deprecated This class will be merged with {@see MariaDBPlatform} in 4.0 because support for MariaDB
* releases prior to 10.4.3 will be dropped.
*/
class MariaDb1027Platform extends MariaDBPlatform
{
}
@@ -0,0 +1,131 @@
<?php
namespace Doctrine\DBAL\Platforms;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\Deprecations\Deprecation;
use function sprintf;
/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.4 database platform.
*
* Extend deprecated MariaDb1027Platform to ensure correct functions used in MySQLSchemaManager which
* tests for MariaDb1027Platform not MariaDBPlatform.
*
* @deprecated This class will be merged with {@see MariaDBPlatform} in 4.0 because support for MariaDB
* releases prior to 10.4.3 will be dropped.
*/
class MariaDb1043Platform extends MariaDb1027Platform
{
/**
* Use JSON rather than LONGTEXT for json columns. Since it is not a true native type, do not override
* hasNativeJsonType() so the DC2Type comment will still be set.
*
* {@inheritDoc}
*/
public function getJsonTypeDeclarationSQL(array $column): string
{
return 'JSON';
}
/**
* {@inheritDoc}
*
* From version 10.4.3, MariaDb aliases JSON to LONGTEXT and adds a constraint CHECK (json_valid). Reverse
* this process when introspecting tables.
*
* @see https://mariadb.com/kb/en/information-schema-check_constraints-table/
* @see https://mariadb.com/kb/en/json-data-type/
* @see https://jira.mariadb.org/browse/MDEV-13916
*/
public function getListTableColumnsSQL($table, $database = null): string
{
// @todo 4.0 - call getColumnTypeSQLSnippet() instead
[$columnTypeSQL, $joinCheckConstraintSQL] = $this->getColumnTypeSQLSnippets('c', $database);
return sprintf(
<<<SQL
SELECT c.COLUMN_NAME AS Field,
$columnTypeSQL AS Type,
c.IS_NULLABLE AS `Null`,
c.COLUMN_KEY AS `Key`,
c.COLUMN_DEFAULT AS `Default`,
c.EXTRA AS Extra,
c.COLUMN_COMMENT AS Comment,
c.CHARACTER_SET_NAME AS CharacterSet,
c.COLLATION_NAME AS Collation
FROM information_schema.COLUMNS c
$joinCheckConstraintSQL
WHERE c.TABLE_SCHEMA = %s
AND c.TABLE_NAME = %s
ORDER BY ORDINAL_POSITION ASC;
SQL
,
$this->getDatabaseNameSQL($database),
$this->quoteStringLiteral($table),
);
}
/**
* Generate SQL snippets to reverse the aliasing of JSON to LONGTEXT.
*
* MariaDb aliases columns specified as JSON to LONGTEXT and sets a CHECK constraint to ensure the column
* is valid json. This function generates the SQL snippets which reverse this aliasing i.e. report a column
* as JSON where it was originally specified as such instead of LONGTEXT.
*
* The CHECK constraints are stored in information_schema.CHECK_CONSTRAINTS so query that table.
*/
public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string
{
if ($this->getJsonTypeDeclarationSQL([]) !== 'JSON') {
return parent::getColumnTypeSQLSnippet($tableAlias, $databaseName);
}
if ($databaseName === null) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6215',
'Not passing a database name to methods "getColumnTypeSQLSnippet()", '
. '"getColumnTypeSQLSnippets()", and "getListTableColumnsSQL()" of "%s" is deprecated.',
self::class,
);
}
$subQueryAlias = 'i_' . $tableAlias;
$databaseName = $this->getDatabaseNameSQL($databaseName);
// The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues
return <<<SQL
IF(
$tableAlias.COLUMN_TYPE = 'longtext'
AND EXISTS(
SELECT * from information_schema.CHECK_CONSTRAINTS $subQueryAlias
WHERE $subQueryAlias.CONSTRAINT_SCHEMA = $databaseName
AND $subQueryAlias.TABLE_NAME = $tableAlias.TABLE_NAME
AND $subQueryAlias.CHECK_CLAUSE = CONCAT(
'json_valid(`',
$tableAlias.COLUMN_NAME,
'`)'
)
),
'json',
$tableAlias.COLUMN_TYPE
)
SQL;
}
/** {@inheritDoc} */
public function getColumnDeclarationSQL($name, array $column)
{
// MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore
// collation and character set for json columns as attempting to set them can cause an error.
if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) {
unset($column['collation']);
unset($column['charset']);
}
return parent::getColumnDeclarationSQL($name, $column);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Doctrine\DBAL\Platforms;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.5 database platform.
*/
class MariaDb1052Platform extends MariaDb1043Platform
{
/**
* {@inheritDoc}
*/
protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
{
return [];
}
/**
* {@inheritDoc}
*/
protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
{
return [];
}
/**
* {@inheritDoc}
*/
protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
{
return ['ALTER TABLE ' . $tableName . ' RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)];
}
}
@@ -0,0 +1,16 @@
<?php
namespace Doctrine\DBAL\Platforms;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.6 database platform.
*/
class MariaDb1060Platform extends MariaDb1052Platform
{
public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}
}
@@ -0,0 +1,99 @@
<?php
namespace Doctrine\DBAL\Platforms;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
/**
* Provides the behavior, features and SQL dialect of the MySQL 5.7 database platform.
*
* @deprecated This class will be merged with {@see MySQLPlatform} in 4.0 because support for MySQL
* releases prior to 5.7 will be dropped.
*/
class MySQL57Platform extends MySQLPlatform
{
/**
* {@inheritDoc}
*
* @deprecated
*/
public function hasNativeJsonType()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5509',
'%s is deprecated.',
__METHOD__,
);
return true;
}
/**
* {@inheritDoc}
*/
public function getJsonTypeDeclarationSQL(array $column)
{
return 'JSON';
}
public function createSQLParser(): Parser
{
return new Parser(true);
}
/**
* {@inheritDoc}
*/
protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
{
return [];
}
/**
* {@inheritDoc}
*/
protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
{
return [];
}
/**
* {@inheritDoc}
*/
protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
{
return ['ALTER TABLE ' . $tableName . ' RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)];
}
/**
* {@inheritDoc}
*
* @deprecated Implement {@see createReservedKeywordsList()} instead.
*/
protected function getReservedKeywordsClass()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4510',
'MySQL57Platform::getReservedKeywordsClass() is deprecated,'
. ' use MySQL57Platform::createReservedKeywordsList() instead.',
);
return Keywords\MySQL57Keywords::class;
}
/**
* {@inheritDoc}
*/
protected function initializeDoctrineTypeMappings()
{
parent::initializeDoctrineTypeMappings();
$this->doctrineTypeMapping['json'] = Types::JSON;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Doctrine\DBAL\Platforms;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\Deprecations\Deprecation;
/**
* Provides the behavior, features and SQL dialect of the MySQL 8.0 database platform.
*/
class MySQL80Platform extends MySQL57Platform
{
/**
* {@inheritDoc}
*
* @deprecated Implement {@see createReservedKeywordsList()} instead.
*/
protected function getReservedKeywordsClass()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4510',
'MySQL80Platform::getReservedKeywordsClass() is deprecated,'
. ' use MySQL80Platform::createReservedKeywordsList() instead.',
);
return Keywords\MySQL80Keywords::class;
}
public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}
}
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,339 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
use Doctrine\DBAL\Driver\Exception as DriverException;
use Doctrine\DBAL\Driver\Result as DriverResult;
use Doctrine\DBAL\Exception\NoKeyValue;
use Doctrine\Deprecations\Deprecation;
use LogicException;
use Traversable;
use function array_shift;
use function func_num_args;
class Result
{
private DriverResult $result;
private Connection $connection;
/** @internal The result can be only instantiated by {@see Connection} or {@see Statement}. */
public function __construct(DriverResult $result, Connection $connection)
{
$this->result = $result;
$this->connection = $connection;
}
/**
* Returns the next row of the result as a numeric array or FALSE if there are no more rows.
*
* @return list<mixed>|false
*
* @throws Exception
*/
public function fetchNumeric()
{
try {
return $this->result->fetchNumeric();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* Returns the next row of the result as an associative array or FALSE if there are no more rows.
*
* @return array<string,mixed>|false
*
* @throws Exception
*/
public function fetchAssociative()
{
try {
return $this->result->fetchAssociative();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* Returns the first value of the next row of the result or FALSE if there are no more rows.
*
* @return mixed|false
*
* @throws Exception
*/
public function fetchOne()
{
try {
return $this->result->fetchOne();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* Returns an array containing all of the result rows represented as numeric arrays.
*
* @return list<list<mixed>>
*
* @throws Exception
*/
public function fetchAllNumeric(): array
{
try {
return $this->result->fetchAllNumeric();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* Returns an array containing all of the result rows represented as associative arrays.
*
* @return list<array<string,mixed>>
*
* @throws Exception
*/
public function fetchAllAssociative(): array
{
try {
return $this->result->fetchAllAssociative();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* Returns an array containing the values of the first column of the result.
*
* @return array<mixed,mixed>
*
* @throws Exception
*/
public function fetchAllKeyValue(): array
{
$this->ensureHasKeyValue();
$data = [];
foreach ($this->fetchAllNumeric() as [$key, $value]) {
$data[$key] = $value;
}
return $data;
}
/**
* Returns an associative array with the keys mapped to the first column and the values being
* an associative array representing the rest of the columns and their values.
*
* @return array<mixed,array<string,mixed>>
*
* @throws Exception
*/
public function fetchAllAssociativeIndexed(): array
{
$data = [];
foreach ($this->fetchAllAssociative() as $row) {
$data[array_shift($row)] = $row;
}
return $data;
}
/**
* @return list<mixed>
*
* @throws Exception
*/
public function fetchFirstColumn(): array
{
try {
return $this->result->fetchFirstColumn();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/**
* @return Traversable<int,list<mixed>>
*
* @throws Exception
*/
public function iterateNumeric(): Traversable
{
while (($row = $this->fetchNumeric()) !== false) {
yield $row;
}
}
/**
* @return Traversable<int,array<string,mixed>>
*
* @throws Exception
*/
public function iterateAssociative(): Traversable
{
while (($row = $this->fetchAssociative()) !== false) {
yield $row;
}
}
/**
* @return Traversable<mixed, mixed>
*
* @throws Exception
*/
public function iterateKeyValue(): Traversable
{
$this->ensureHasKeyValue();
foreach ($this->iterateNumeric() as [$key, $value]) {
yield $key => $value;
}
}
/**
* Returns an iterator over the result set with the keys mapped to the first column and the values being
* an associative array representing the rest of the columns and their values.
*
* @return Traversable<mixed,array<string,mixed>>
*
* @throws Exception
*/
public function iterateAssociativeIndexed(): Traversable
{
foreach ($this->iterateAssociative() as $row) {
yield array_shift($row) => $row;
}
}
/**
* @return Traversable<int,mixed>
*
* @throws Exception
*/
public function iterateColumn(): Traversable
{
while (($value = $this->fetchOne()) !== false) {
yield $value;
}
}
/** @throws Exception */
public function rowCount(): int
{
try {
return $this->result->rowCount();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
/** @throws Exception */
public function columnCount(): int
{
try {
return $this->result->columnCount();
} catch (DriverException $e) {
throw $this->connection->convertException($e);
}
}
public function free(): void
{
$this->result->free();
}
/** @throws Exception */
private function ensureHasKeyValue(): void
{
$columnCount = $this->columnCount();
if ($columnCount < 2) {
throw NoKeyValue::fromColumnCount($columnCount);
}
}
/**
* BC layer for a wide-spread use-case of old DBAL APIs
*
* @deprecated Use {@see fetchNumeric()}, {@see fetchAssociative()} or {@see fetchOne()} instead.
*
* @psalm-param FetchMode::* $mode
*
* @return mixed
*
* @throws Exception
*/
public function fetch(int $mode = FetchMode::ASSOCIATIVE)
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4007',
'%s is deprecated, please use fetchNumeric(), fetchAssociative() or fetchOne() instead.',
__METHOD__,
);
if (func_num_args() > 1) {
throw new LogicException('Only invocations with one argument are still supported by this legacy API.');
}
if ($mode === FetchMode::ASSOCIATIVE) {
return $this->fetchAssociative();
}
if ($mode === FetchMode::NUMERIC) {
return $this->fetchNumeric();
}
if ($mode === FetchMode::COLUMN) {
return $this->fetchOne();
}
throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.');
}
/**
* BC layer for a wide-spread use-case of old DBAL APIs
*
* @deprecated Use {@see fetchAllNumeric()}, {@see fetchAllAssociative()} or {@see fetchFirstColumn()} instead.
*
* @psalm-param FetchMode::* $mode
*
* @return list<mixed>
*
* @throws Exception
*/
public function fetchAll(int $mode = FetchMode::ASSOCIATIVE): array
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4007',
'%s is deprecated, please use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.',
__METHOD__,
);
if (func_num_args() > 1) {
throw new LogicException('Only invocations with one argument are still supported by this legacy API.');
}
if ($mode === FetchMode::ASSOCIATIVE) {
return $this->fetchAllAssociative();
}
if ($mode === FetchMode::NUMERIC) {
return $this->fetchAllNumeric();
}
if ($mode === FetchMode::COLUMN) {
return $this->fetchFirstColumn();
}
throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.');
}
}
@@ -0,0 +1,224 @@
<?php
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use function array_map;
use function crc32;
use function dechex;
use function explode;
use function implode;
use function str_replace;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;
/**
* The abstract asset allows to reset the name of all assets without publishing this to the public userland.
*
* This encapsulation hack is necessary to keep a consistent state of the database schema. Say we have a list of tables
* array($tableName => Table($tableName)); if you want to rename the table, you have to make sure this does not get
* recreated during schema migration.
*/
abstract class AbstractAsset
{
/** @var string */
protected $_name = '';
/**
* Namespace of the asset. If none isset the default namespace is assumed.
*
* @var string|null
*/
protected $_namespace;
/** @var bool */
protected $_quoted = false;
/**
* Sets the name of this asset.
*
* @param string $name
*
* @return void
*/
protected function _setName($name)
{
if ($this->isIdentifierQuoted($name)) {
$this->_quoted = true;
$name = $this->trimQuotes($name);
}
if (strpos($name, '.') !== false) {
$parts = explode('.', $name);
$this->_namespace = $parts[0];
$name = $parts[1];
}
$this->_name = $name;
}
/**
* Is this asset in the default namespace?
*
* @param string $defaultNamespaceName
*
* @return bool
*/
public function isInDefaultNamespace($defaultNamespaceName)
{
return $this->_namespace === $defaultNamespaceName || $this->_namespace === null;
}
/**
* Gets the namespace name of this asset.
*
* If NULL is returned this means the default namespace is used.
*
* @return string|null
*/
public function getNamespaceName()
{
return $this->_namespace;
}
/**
* The shortest name is stripped of the default namespace. All other
* namespaced elements are returned as full-qualified names.
*
* @param string|null $defaultNamespaceName
*
* @return string
*/
public function getShortestName($defaultNamespaceName)
{
$shortestName = $this->getName();
if ($this->_namespace === $defaultNamespaceName) {
$shortestName = $this->_name;
}
return strtolower($shortestName);
}
/**
* The normalized name is full-qualified and lower-cased. Lower-casing is
* actually wrong, but we have to do it to keep our sanity. If you are
* using database objects that only differentiate in the casing (FOO vs
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
*
* Every non-namespaced element is prefixed with the default namespace
* name which is passed as argument to this method.
*
* @deprecated Use {@see getNamespaceName()} and {@see getName()} instead.
*
* @param string $defaultNamespaceName
*
* @return string
*/
public function getFullQualifiedName($defaultNamespaceName)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4814',
'AbstractAsset::getFullQualifiedName() is deprecated.'
. ' Use AbstractAsset::getNamespaceName() and ::getName() instead.',
);
$name = $this->getName();
if ($this->_namespace === null) {
$name = $defaultNamespaceName . '.' . $name;
}
return strtolower($name);
}
/**
* Checks if this asset's name is quoted.
*
* @return bool
*/
public function isQuoted()
{
return $this->_quoted;
}
/**
* Checks if this identifier is quoted.
*
* @param string $identifier
*
* @return bool
*/
protected function isIdentifierQuoted($identifier)
{
return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
}
/**
* Trim quotes from the identifier.
*
* @param string $identifier
*
* @return string
*/
protected function trimQuotes($identifier)
{
return str_replace(['`', '"', '[', ']'], '', $identifier);
}
/**
* Returns the name of this schema asset.
*
* @return string
*/
public function getName()
{
if ($this->_namespace !== null) {
return $this->_namespace . '.' . $this->_name;
}
return $this->_name;
}
/**
* Gets the quoted representation of this asset but only if it was defined with one. Otherwise
* return the plain unquoted value as inserted.
*
* @return string
*/
public function getQuotedName(AbstractPlatform $platform)
{
$keywords = $platform->getReservedKeywordsList();
$parts = explode('.', $this->getName());
foreach ($parts as $k => $v) {
$parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v;
}
return implode('.', $parts);
}
/**
* Generates an identifier from a list of column names obeying a certain string length.
*
* This is especially important for Oracle, since it does not allow identifiers larger than 30 chars,
* however building idents automatically for foreign keys, composite keys or such can easily create
* very long names.
*
* @param string[] $columnNames
* @param string $prefix
* @param int $maxSize
*
* @return string
*/
protected function _generateIdentifierName($columnNames, $prefix = '', $maxSize = 30)
{
$hash = implode('', array_map(static function ($column): string {
return dechex(crc32($column));
}, $columnNames));
return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,605 @@
<?php
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MySQL;
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use function array_change_key_case;
use function array_shift;
use function assert;
use function explode;
use function implode;
use function is_string;
use function preg_match;
use function strpos;
use function strtok;
use function strtolower;
use function strtr;
use const CASE_LOWER;
/**
* Schema manager for the MySQL RDBMS.
*
* @extends AbstractSchemaManager<AbstractMySQLPlatform>
*/
class MySQLSchemaManager extends AbstractSchemaManager
{
/** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */
private const MARIADB_ESCAPE_SEQUENCES = [
'\\0' => "\0",
"\\'" => "'",
'\\"' => '"',
'\\b' => "\b",
'\\n' => "\n",
'\\r' => "\r",
'\\t' => "\t",
'\\Z' => "\x1a",
'\\\\' => '\\',
'\\%' => '%',
'\\_' => '_',
// Internally, MariaDB escapes single quotes using the standard syntax
"''" => "'",
];
/**
* {@inheritDoc}
*/
public function listTableNames()
{
return $this->doListTableNames();
}
/**
* {@inheritDoc}
*/
public function listTables()
{
return $this->doListTables();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see introspectTable()} instead.
*/
public function listTableDetails($name)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5595',
'%s is deprecated. Use introspectTable() instead.',
__METHOD__,
);
return $this->doListTableDetails($name);
}
/**
* {@inheritDoc}
*/
public function listTableColumns($table, $database = null)
{
return $this->doListTableColumns($table, $database);
}
/**
* {@inheritDoc}
*/
public function listTableIndexes($table)
{
return $this->doListTableIndexes($table);
}
/**
* {@inheritDoc}
*/
public function listTableForeignKeys($table, $database = null)
{
return $this->doListTableForeignKeys($table, $database);
}
/**
* {@inheritDoc}
*/
protected function _getPortableViewDefinition($view)
{
return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableDefinition($table)
{
return array_shift($table);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
{
foreach ($tableIndexes as $k => $v) {
$v = array_change_key_case($v, CASE_LOWER);
if ($v['key_name'] === 'PRIMARY') {
$v['primary'] = true;
} else {
$v['primary'] = false;
}
if (strpos($v['index_type'], 'FULLTEXT') !== false) {
$v['flags'] = ['FULLTEXT'];
} elseif (strpos($v['index_type'], 'SPATIAL') !== false) {
$v['flags'] = ['SPATIAL'];
}
// Ignore prohibited prefix `length` for spatial index
if (strpos($v['index_type'], 'SPATIAL') === false) {
$v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
}
$tableIndexes[$k] = $v;
}
return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
}
/**
* {@inheritDoc}
*/
protected function _getPortableDatabaseDefinition($database)
{
return $database['Database'];
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableColumnDefinition($tableColumn)
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$dbType = strtolower($tableColumn['type']);
$dbType = strtok($dbType, '(), ');
assert(is_string($dbType));
$length = $tableColumn['length'] ?? strtok('(), ');
$fixed = null;
if (! isset($tableColumn['name'])) {
$tableColumn['name'] = '';
}
$scale = null;
$precision = null;
$type = $origType = $this->_platform->getDoctrineTypeMapping($dbType);
// In cases where not connected to a database DESCRIBE $table does not return 'Comment'
if (isset($tableColumn['comment'])) {
$type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
$tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
}
switch ($dbType) {
case 'char':
case 'binary':
$fixed = true;
break;
case 'float':
case 'double':
case 'real':
case 'numeric':
case 'decimal':
if (
preg_match(
'([A-Za-z]+\(([0-9]+),([0-9]+)\))',
$tableColumn['type'],
$match,
) === 1
) {
$precision = $match[1];
$scale = $match[2];
$length = null;
}
break;
case 'tinytext':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
break;
case 'text':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
break;
case 'mediumtext':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
break;
case 'tinyblob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
break;
case 'blob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
break;
case 'mediumblob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
break;
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'integer':
case 'bigint':
case 'year':
$length = null;
break;
}
if ($this->_platform instanceof MariaDb1027Platform) {
$columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']);
} else {
$columnDefault = $tableColumn['default'];
}
$options = [
'length' => $length !== null ? (int) $length : null,
'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false,
'fixed' => (bool) $fixed,
'default' => $columnDefault,
'notnull' => $tableColumn['null'] !== 'YES',
'scale' => null,
'precision' => null,
'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false,
'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
? $tableColumn['comment']
: null,
];
if ($scale !== null && $precision !== null) {
$options['scale'] = (int) $scale;
$options['precision'] = (int) $precision;
}
$column = new Column($tableColumn['field'], Type::getType($type), $options);
if (isset($tableColumn['characterset'])) {
$column->setPlatformOption('charset', $tableColumn['characterset']);
}
if (isset($tableColumn['collation'])) {
$column->setPlatformOption('collation', $tableColumn['collation']);
}
if (isset($tableColumn['declarationMismatch'])) {
$column->setPlatformOption('declarationMismatch', $tableColumn['declarationMismatch']);
}
// Check underlying database type where doctrine type is inferred from DC2Type comment
// and set a flag if it is not as expected.
if ($type === 'json' && $origType !== $type && $this->expectedDbType($type, $options) !== $dbType) {
$column->setPlatformOption('declarationMismatch', true);
}
return $column;
}
/**
* Returns the database data type for a given doctrine type and column
*
* Note that for data types that depend on length where length is not part of the column definition
* and therefore the $tableColumn['length'] will not be set, for example TEXT (which could be LONGTEXT,
* MEDIUMTEXT) or BLOB (LONGBLOB or TINYBLOB), the expectedDbType cannot be inferred exactly, merely
* the default type.
*
* This method is intended to be used to determine underlying database type where doctrine type is
* inferred from a DC2Type comment.
*
* @param mixed[] $tableColumn
*/
private function expectedDbType(string $type, array $tableColumn): string
{
$_type = Type::getType($type);
$expectedDbType = strtolower($_type->getSQLDeclaration($tableColumn, $this->_platform));
$expectedDbType = strtok($expectedDbType, '(), ');
return $expectedDbType === false ? '' : $expectedDbType;
}
/**
* Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
*
* - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
* to distinguish them from expressions (see MDEV-10134).
* - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
* as current_timestamp(), currdate(), currtime()
* - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
* null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
* - \' is always stored as '' in information_schema (normalized)
*
* @link https://mariadb.com/kb/en/library/information-schema-columns-table/
* @link https://jira.mariadb.org/browse/MDEV-13132
*
* @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
*/
private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
{
if ($columnDefault === 'NULL' || $columnDefault === null) {
return null;
}
if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) {
return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
}
switch ($columnDefault) {
case 'current_timestamp()':
return $platform->getCurrentTimestampSQL();
case 'curdate()':
return $platform->getCurrentDateSQL();
case 'curtime()':
return $platform->getCurrentTimeSQL();
}
return $columnDefault;
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeysList($tableForeignKeys)
{
$list = [];
foreach ($tableForeignKeys as $value) {
$value = array_change_key_case($value, CASE_LOWER);
if (! isset($list[$value['constraint_name']])) {
if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
$value['delete_rule'] = null;
}
if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
$value['update_rule'] = null;
}
$list[$value['constraint_name']] = [
'name' => $value['constraint_name'],
'local' => [],
'foreign' => [],
'foreignTable' => $value['referenced_table_name'],
'onDelete' => $value['delete_rule'],
'onUpdate' => $value['update_rule'],
];
}
$list[$value['constraint_name']]['local'][] = $value['column_name'];
$list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
}
return parent::_getPortableTableForeignKeysList($list);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint
{
return new ForeignKeyConstraint(
$tableForeignKey['local'],
$tableForeignKey['foreignTable'],
$tableForeignKey['foreign'],
$tableForeignKey['name'],
[
'onDelete' => $tableForeignKey['onDelete'],
'onUpdate' => $tableForeignKey['onUpdate'],
],
);
}
public function createComparator(): Comparator
{
return new MySQL\Comparator(
$this->_platform,
new CachingCollationMetadataProvider(
new ConnectionCollationMetadataProvider($this->_conn),
),
);
}
protected function selectTableNames(string $databaseName): Result
{
$sql = <<<'SQL'
SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
AND TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_NAME
SQL;
return $this->_conn->executeQuery($sql, [$databaseName]);
}
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
{
// @todo 4.0 - call getColumnTypeSQLSnippet() instead
[$columnTypeSQL, $joinCheckConstraintSQL] = $this->_platform->getColumnTypeSQLSnippets('c', $databaseName);
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' c.TABLE_NAME,';
}
$sql .= <<<SQL
c.COLUMN_NAME AS field,
$columnTypeSQL AS type,
c.IS_NULLABLE AS `null`,
c.COLUMN_KEY AS `key`,
c.COLUMN_DEFAULT AS `default`,
c.EXTRA,
c.COLUMN_COMMENT AS comment,
c.CHARACTER_SET_NAME AS characterset,
c.COLLATION_NAME AS collation
FROM information_schema.COLUMNS c
INNER JOIN information_schema.TABLES t
ON t.TABLE_NAME = c.TABLE_NAME
$joinCheckConstraintSQL
SQL;
// The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
// in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
// caused by https://bugs.mysql.com/bug.php?id=81347
$conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?', "t.TABLE_TYPE = 'BASE TABLE'"];
$params = [$databaseName, $databaseName];
if ($tableName !== null) {
$conditions[] = 't.TABLE_NAME = ?';
$params[] = $tableName;
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY ORDINAL_POSITION';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' TABLE_NAME,';
}
$sql .= <<<'SQL'
NON_UNIQUE AS Non_Unique,
INDEX_NAME AS Key_name,
COLUMN_NAME AS Column_Name,
SUB_PART AS Sub_Part,
INDEX_TYPE AS Index_Type
FROM information_schema.STATISTICS
SQL;
$conditions = ['TABLE_SCHEMA = ?'];
$params = [$databaseName];
if ($tableName !== null) {
$conditions[] = 'TABLE_NAME = ?';
$params[] = $tableName;
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT DISTINCT';
if ($tableName === null) {
$sql .= ' k.TABLE_NAME,';
}
$sql .= <<<'SQL'
k.CONSTRAINT_NAME,
k.COLUMN_NAME,
k.REFERENCED_TABLE_NAME,
k.REFERENCED_COLUMN_NAME,
k.ORDINAL_POSITION /*!50116,
c.UPDATE_RULE,
c.DELETE_RULE */
FROM information_schema.key_column_usage k /*!50116
INNER JOIN information_schema.referential_constraints c
ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
AND c.TABLE_NAME = k.TABLE_NAME */
SQL;
$conditions = ['k.TABLE_SCHEMA = ?'];
$params = [$databaseName];
if ($tableName !== null) {
$conditions[] = 'k.TABLE_NAME = ?';
$params[] = $tableName;
}
$conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL';
$sql .= ' WHERE ' . implode(' AND ', $conditions)
// The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
// in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
// caused by https://bugs.mysql.com/bug.php?id=81347.
// Use a string literal for the database name since the internal PDO SQL parser
// cannot recognize parameter placeholders inside conditional comments
. ' /*!50116 AND c.CONSTRAINT_SCHEMA = ' . $this->_conn->quote($databaseName) . ' */'
. ' ORDER BY k.ORDINAL_POSITION';
return $this->_conn->executeQuery($sql, $params);
}
/**
* {@inheritDoc}
*/
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
{
$sql = $this->_platform->fetchTableOptionsByTable($tableName !== null);
$params = [$databaseName];
if ($tableName !== null) {
$params[] = $tableName;
}
/** @var array<string,array<string,mixed>> $metadata */
$metadata = $this->_conn->executeQuery($sql, $params)
->fetchAllAssociativeIndexed();
$tableOptions = [];
foreach ($metadata as $table => $data) {
$data = array_change_key_case($data, CASE_LOWER);
$tableOptions[$table] = [
'engine' => $data['engine'],
'collation' => $data['table_collation'],
'charset' => $data['character_set_name'],
'autoincrement' => $data['auto_increment'],
'comment' => $data['table_comment'],
'create_options' => $this->parseCreateOptions($data['create_options']),
];
}
return $tableOptions;
}
/** @return string[]|true[] */
private function parseCreateOptions(?string $string): array
{
$options = [];
if ($string === null || $string === '') {
return $options;
}
foreach (explode(' ', $string) as $pair) {
$parts = explode('=', $pair, 2);
$options[$parts[0]] = $parts[1] ?? true;
}
return $options;
}
}
@@ -0,0 +1,537 @@
<?php
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use function array_change_key_case;
use function array_values;
use function implode;
use function is_string;
use function preg_match;
use function str_replace;
use function strpos;
use function strtolower;
use function strtoupper;
use function trim;
use const CASE_LOWER;
/**
* Oracle Schema Manager.
*
* @extends AbstractSchemaManager<OraclePlatform>
*/
class OracleSchemaManager extends AbstractSchemaManager
{
/**
* {@inheritDoc}
*/
public function listTableNames()
{
return $this->doListTableNames();
}
/**
* {@inheritDoc}
*/
public function listTables()
{
return $this->doListTables();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see introspectTable()} instead.
*/
public function listTableDetails($name)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5595',
'%s is deprecated. Use introspectTable() instead.',
__METHOD__,
);
return $this->doListTableDetails($name);
}
/**
* {@inheritDoc}
*/
public function listTableColumns($table, $database = null)
{
return $this->doListTableColumns($table, $database);
}
/**
* {@inheritDoc}
*/
public function listTableIndexes($table)
{
return $this->doListTableIndexes($table);
}
/**
* {@inheritDoc}
*/
public function listTableForeignKeys($table, $database = null)
{
return $this->doListTableForeignKeys($table, $database);
}
/**
* {@inheritDoc}
*/
protected function _getPortableViewDefinition($view)
{
$view = array_change_key_case($view, CASE_LOWER);
return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableDefinition($table)
{
$table = array_change_key_case($table, CASE_LOWER);
return $this->getQuotedIdentifierName($table['table_name']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
{
$indexBuffer = [];
foreach ($tableIndexes as $tableIndex) {
$tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
$keyName = strtolower($tableIndex['name']);
$buffer = [];
if ($tableIndex['is_primary'] === 'P') {
$keyName = 'primary';
$buffer['primary'] = true;
$buffer['non_unique'] = false;
} else {
$buffer['primary'] = false;
$buffer['non_unique'] = ! $tableIndex['is_unique'];
}
$buffer['key_name'] = $keyName;
$buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
$indexBuffer[] = $buffer;
}
return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableColumnDefinition($tableColumn)
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$dbType = strtolower($tableColumn['data_type']);
if (strpos($dbType, 'timestamp(') === 0) {
if (strpos($dbType, 'with time zone') !== false) {
$dbType = 'timestamptz';
} else {
$dbType = 'timestamp';
}
}
$unsigned = $fixed = $precision = $scale = $length = null;
if (! isset($tableColumn['column_name'])) {
$tableColumn['column_name'] = '';
}
// Default values returned from database sometimes have trailing spaces.
if (is_string($tableColumn['data_default'])) {
$tableColumn['data_default'] = trim($tableColumn['data_default']);
}
if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
$tableColumn['data_default'] = null;
}
if ($tableColumn['data_default'] !== null) {
// Default values returned from database are represented as literal expressions
if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) {
$tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
}
}
if ($tableColumn['data_precision'] !== null) {
$precision = (int) $tableColumn['data_precision'];
}
if ($tableColumn['data_scale'] !== null) {
$scale = (int) $tableColumn['data_scale'];
}
$type = $this->_platform->getDoctrineTypeMapping($dbType);
$type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
$tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);
switch ($dbType) {
case 'number':
if ($precision === 20 && $scale === 0) {
$type = 'bigint';
} elseif ($precision === 5 && $scale === 0) {
$type = 'smallint';
} elseif ($precision === 1 && $scale === 0) {
$type = 'boolean';
} elseif ($scale > 0) {
$type = 'decimal';
}
break;
case 'varchar':
case 'varchar2':
case 'nvarchar2':
$length = $tableColumn['char_length'];
$fixed = false;
break;
case 'raw':
$length = $tableColumn['data_length'];
$fixed = true;
break;
case 'char':
case 'nchar':
$length = $tableColumn['char_length'];
$fixed = true;
break;
}
$options = [
'notnull' => $tableColumn['nullable'] === 'N',
'fixed' => (bool) $fixed,
'unsigned' => (bool) $unsigned,
'default' => $tableColumn['data_default'],
'length' => $length,
'precision' => $precision,
'scale' => $scale,
'comment' => isset($tableColumn['comments']) && $tableColumn['comments'] !== ''
? $tableColumn['comments']
: null,
];
return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeysList($tableForeignKeys)
{
$list = [];
foreach ($tableForeignKeys as $value) {
$value = array_change_key_case($value, CASE_LOWER);
if (! isset($list[$value['constraint_name']])) {
if ($value['delete_rule'] === 'NO ACTION') {
$value['delete_rule'] = null;
}
$list[$value['constraint_name']] = [
'name' => $this->getQuotedIdentifierName($value['constraint_name']),
'local' => [],
'foreign' => [],
'foreignTable' => $value['references_table'],
'onDelete' => $value['delete_rule'],
];
}
$localColumn = $this->getQuotedIdentifierName($value['local_column']);
$foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);
$list[$value['constraint_name']]['local'][$value['position']] = $localColumn;
$list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
}
return parent::_getPortableTableForeignKeysList($list);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint
{
return new ForeignKeyConstraint(
array_values($tableForeignKey['local']),
$this->getQuotedIdentifierName($tableForeignKey['foreignTable']),
array_values($tableForeignKey['foreign']),
$this->getQuotedIdentifierName($tableForeignKey['name']),
['onDelete' => $tableForeignKey['onDelete']],
);
}
/**
* {@inheritDoc}
*/
protected function _getPortableSequenceDefinition($sequence)
{
$sequence = array_change_key_case($sequence, CASE_LOWER);
return new Sequence(
$this->getQuotedIdentifierName($sequence['sequence_name']),
(int) $sequence['increment_by'],
(int) $sequence['min_value'],
);
}
/**
* {@inheritDoc}
*/
protected function _getPortableDatabaseDefinition($database)
{
$database = array_change_key_case($database, CASE_LOWER);
return $database['username'];
}
/**
* {@inheritDoc}
*/
public function createDatabase($database)
{
$statement = $this->_platform->getCreateDatabaseSQL($database);
$params = $this->_conn->getParams();
if (isset($params['password'])) {
$statement .= ' IDENTIFIED BY ' . $params['password'];
}
$this->_conn->executeStatement($statement);
$statement = 'GRANT DBA TO ' . $database;
$this->_conn->executeStatement($statement);
}
/**
* @internal The method should be only used from within the OracleSchemaManager class hierarchy.
*
* @param string $table
*
* @return bool
*
* @throws Exception
*/
public function dropAutoincrement($table)
{
$sql = $this->_platform->getDropAutoincrementSql($table);
foreach ($sql as $query) {
$this->_conn->executeStatement($query);
}
return true;
}
/**
* {@inheritDoc}
*/
public function dropTable($name)
{
$this->tryMethod('dropAutoincrement', $name);
parent::dropTable($name);
}
/**
* Returns the quoted representation of the given identifier name.
*
* Quotes non-uppercase identifiers explicitly to preserve case
* and thus make references to the particular identifier work.
*
* @param string $identifier The identifier to quote.
*/
private function getQuotedIdentifierName($identifier): string
{
if (preg_match('/[a-z]/', $identifier) === 1) {
return $this->_platform->quoteIdentifier($identifier);
}
return $identifier;
}
protected function selectTableNames(string $databaseName): Result
{
$sql = <<<'SQL'
SELECT TABLE_NAME
FROM ALL_TABLES
WHERE OWNER = :OWNER
ORDER BY TABLE_NAME
SQL;
return $this->_conn->executeQuery($sql, ['OWNER' => $databaseName]);
}
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' C.TABLE_NAME,';
}
$sql .= <<<'SQL'
C.COLUMN_NAME,
C.DATA_TYPE,
C.DATA_DEFAULT,
C.DATA_PRECISION,
C.DATA_SCALE,
C.CHAR_LENGTH,
C.DATA_LENGTH,
C.NULLABLE,
D.COMMENTS
FROM ALL_TAB_COLUMNS C
INNER JOIN ALL_TABLES T
ON T.OWNER = C.OWNER
AND T.TABLE_NAME = C.TABLE_NAME
LEFT JOIN ALL_COL_COMMENTS D
ON D.OWNER = C.OWNER
AND D.TABLE_NAME = C.TABLE_NAME
AND D.COLUMN_NAME = C.COLUMN_NAME
SQL;
$conditions = ['C.OWNER = :OWNER'];
$params = ['OWNER' => $databaseName];
if ($tableName !== null) {
$conditions[] = 'C.TABLE_NAME = :TABLE_NAME';
$params['TABLE_NAME'] = $tableName;
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' IND_COL.TABLE_NAME,';
}
$sql .= <<<'SQL'
IND_COL.INDEX_NAME AS NAME,
IND.INDEX_TYPE AS TYPE,
DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE,
IND_COL.COLUMN_NAME,
IND_COL.COLUMN_POSITION AS COLUMN_POS,
CON.CONSTRAINT_TYPE AS IS_PRIMARY
FROM ALL_IND_COLUMNS IND_COL
LEFT JOIN ALL_INDEXES IND
ON IND.OWNER = IND_COL.INDEX_OWNER
AND IND.INDEX_NAME = IND_COL.INDEX_NAME
LEFT JOIN ALL_CONSTRAINTS CON
ON CON.OWNER = IND_COL.INDEX_OWNER
AND CON.INDEX_NAME = IND_COL.INDEX_NAME
SQL;
$conditions = ['IND_COL.INDEX_OWNER = :OWNER'];
$params = ['OWNER' => $databaseName];
if ($tableName !== null) {
$conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME';
$params['TABLE_NAME'] = $tableName;
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME'
. ', IND_COL.COLUMN_POSITION';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' COLS.TABLE_NAME,';
}
$sql .= <<<'SQL'
ALC.CONSTRAINT_NAME,
ALC.DELETE_RULE,
COLS.COLUMN_NAME LOCAL_COLUMN,
COLS.POSITION,
R_COLS.TABLE_NAME REFERENCES_TABLE,
R_COLS.COLUMN_NAME FOREIGN_COLUMN
FROM ALL_CONS_COLUMNS COLS
LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND
R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND
R_COLS.POSITION = COLS.POSITION
SQL;
$conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER'];
$params = ['OWNER' => $databaseName];
if ($tableName !== null) {
$conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME';
$params['TABLE_NAME'] = $tableName;
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME'
. ', COLS.POSITION';
return $this->_conn->executeQuery($sql, $params);
}
/**
* {@inheritDoc}
*/
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
{
$sql = 'SELECT TABLE_NAME, COMMENTS';
$conditions = ['OWNER = :OWNER'];
$params = ['OWNER' => $databaseName];
if ($tableName !== null) {
$conditions[] = 'TABLE_NAME = :TABLE_NAME';
$params['TABLE_NAME'] = $tableName;
}
$sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions);
/** @var array<string,array<string,mixed>> $metadata */
$metadata = $this->_conn->executeQuery($sql, $params)
->fetchAllAssociativeIndexed();
$tableOptions = [];
foreach ($metadata as $table => $data) {
$data = array_change_key_case($data, CASE_LOWER);
$tableOptions[$table] = [
'comment' => $data['comments'],
];
}
return $tableOptions;
}
protected function normalizeName(string $name): string
{
$identifier = new Identifier($name);
return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name);
}
}
@@ -0,0 +1,769 @@
<?php
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use function array_change_key_case;
use function array_filter;
use function array_map;
use function array_merge;
use function array_shift;
use function assert;
use function explode;
use function get_class;
use function implode;
use function in_array;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function trim;
use const CASE_LOWER;
/**
* PostgreSQL Schema Manager.
*
* @extends AbstractSchemaManager<PostgreSQLPlatform>
*/
class PostgreSQLSchemaManager extends AbstractSchemaManager
{
/** @var string[]|null */
private ?array $existingSchemaPaths = null;
/**
* {@inheritDoc}
*/
public function listTableNames()
{
return $this->doListTableNames();
}
/**
* {@inheritDoc}
*/
public function listTables()
{
return $this->doListTables();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see introspectTable()} instead.
*/
public function listTableDetails($name)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5595',
'%s is deprecated. Use introspectTable() instead.',
__METHOD__,
);
return $this->doListTableDetails($name);
}
/**
* {@inheritDoc}
*/
public function listTableColumns($table, $database = null)
{
return $this->doListTableColumns($table, $database);
}
/**
* {@inheritDoc}
*/
public function listTableIndexes($table)
{
return $this->doListTableIndexes($table);
}
/**
* {@inheritDoc}
*/
public function listTableForeignKeys($table, $database = null)
{
return $this->doListTableForeignKeys($table, $database);
}
/**
* Gets all the existing schema names.
*
* @deprecated Use {@see listSchemaNames()} instead.
*
* @return string[]
*
* @throws Exception
*/
public function getSchemaNames()
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4503',
'PostgreSQLSchemaManager::getSchemaNames() is deprecated,'
. ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
);
return $this->listNamespaceNames();
}
/**
* {@inheritDoc}
*/
public function listSchemaNames(): array
{
return $this->_conn->fetchFirstColumn(
<<<'SQL'
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT LIKE 'pg\_%'
AND schema_name != 'information_schema'
SQL,
);
}
/**
* {@inheritDoc}
*
* @deprecated
*/
public function getSchemaSearchPaths()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4821',
'PostgreSQLSchemaManager::getSchemaSearchPaths() is deprecated.',
);
$params = $this->_conn->getParams();
$searchPaths = $this->_conn->fetchOne('SHOW search_path');
assert($searchPaths !== false);
$schema = explode(',', $searchPaths);
if (isset($params['user'])) {
$schema = str_replace('"$user"', $params['user'], $schema);
}
return array_map('trim', $schema);
}
/**
* Gets names of all existing schemas in the current users search path.
*
* This is a PostgreSQL only function.
*
* @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy.
*
* @return string[]
*
* @throws Exception
*/
public function getExistingSchemaSearchPaths()
{
if ($this->existingSchemaPaths === null) {
$this->determineExistingSchemaSearchPaths();
}
assert($this->existingSchemaPaths !== null);
return $this->existingSchemaPaths;
}
/**
* Returns the name of the current schema.
*
* @return string|null
*
* @throws Exception
*/
protected function getCurrentSchema()
{
$schemas = $this->getExistingSchemaSearchPaths();
return array_shift($schemas);
}
/**
* Sets or resets the order of the existing schemas in the current search path of the user.
*
* This is a PostgreSQL only function.
*
* @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy.
*
* @return void
*
* @throws Exception
*/
public function determineExistingSchemaSearchPaths()
{
$names = $this->listSchemaNames();
$paths = $this->getSchemaSearchPaths();
$this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names): bool {
return in_array($v, $names, true);
});
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
{
$onUpdate = null;
$onDelete = null;
if (
preg_match(
'(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
$tableForeignKey['condef'],
$match,
) === 1
) {
$onUpdate = $match[1];
}
if (
preg_match(
'(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
$tableForeignKey['condef'],
$match,
) === 1
) {
$onDelete = $match[1];
}
$result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values);
assert($result === 1);
// PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
// the idea to trim them here.
$localColumns = array_map('trim', explode(',', $values[1]));
$foreignColumns = array_map('trim', explode(',', $values[3]));
$foreignTable = $values[2];
return new ForeignKeyConstraint(
$localColumns,
$foreignTable,
$foreignColumns,
$tableForeignKey['conname'],
['onUpdate' => $onUpdate, 'onDelete' => $onDelete],
);
}
/**
* {@inheritDoc}
*/
protected function _getPortableViewDefinition($view)
{
return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableDefinition($table)
{
$currentSchema = $this->getCurrentSchema();
if ($table['schema_name'] === $currentSchema) {
return $table['table_name'];
}
return $table['schema_name'] . '.' . $table['table_name'];
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
{
$buffer = [];
foreach ($tableIndexes as $row) {
$colNumbers = array_map('intval', explode(' ', $row['indkey']));
$columnNameSql = sprintf(
'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
$row['indrelid'],
implode(' ,', $colNumbers),
);
$indexColumns = $this->_conn->fetchAllAssociative($columnNameSql);
// required for getting the order of the columns right.
foreach ($colNumbers as $colNum) {
foreach ($indexColumns as $colRow) {
if ($colNum !== $colRow['attnum']) {
continue;
}
$buffer[] = [
'key_name' => $row['relname'],
'column_name' => trim($colRow['attname']),
'non_unique' => ! $row['indisunique'],
'primary' => $row['indisprimary'],
'where' => $row['where'],
];
}
}
}
return parent::_getPortableTableIndexesList($buffer, $tableName);
}
/**
* {@inheritDoc}
*/
protected function _getPortableDatabaseDefinition($database)
{
return $database['datname'];
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see listSchemaNames()} instead.
*/
protected function getPortableNamespaceDefinition(array $namespace)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4503',
'PostgreSQLSchemaManager::getPortableNamespaceDefinition() is deprecated,'
. ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
);
return $namespace['nspname'];
}
/**
* {@inheritDoc}
*/
protected function _getPortableSequenceDefinition($sequence)
{
if ($sequence['schemaname'] !== 'public') {
$sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
} else {
$sequenceName = $sequence['relname'];
}
return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableColumnDefinition($tableColumn)
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
// get length from varchar definition
$length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
$tableColumn['length'] = $length;
}
$matches = [];
$autoincrement = false;
if (
$tableColumn['default'] !== null
&& preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches) === 1
) {
$tableColumn['sequence'] = $matches[1];
$tableColumn['default'] = null;
$autoincrement = true;
}
if ($tableColumn['default'] !== null) {
if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) {
$tableColumn['default'] = $matches[1];
} elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) {
$tableColumn['default'] = null;
}
}
$length = $tableColumn['length'] ?? null;
if ($length === '-1' && isset($tableColumn['atttypmod'])) {
$length = $tableColumn['atttypmod'] - 4;
}
if ((int) $length <= 0) {
$length = null;
}
$fixed = null;
if (! isset($tableColumn['name'])) {
$tableColumn['name'] = '';
}
$precision = null;
$scale = null;
$jsonb = null;
$dbType = strtolower($tableColumn['type']);
if (
$tableColumn['domain_type'] !== null
&& $tableColumn['domain_type'] !== ''
&& ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
) {
$dbType = strtolower($tableColumn['domain_type']);
$tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
}
$type = $this->_platform->getDoctrineTypeMapping($dbType);
$type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
$tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
switch ($dbType) {
case 'smallint':
case 'int2':
$tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
$length = null;
break;
case 'int':
case 'int4':
case 'integer':
$tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
$length = null;
break;
case 'bigint':
case 'int8':
$tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
$length = null;
break;
case 'bool':
case 'boolean':
if ($tableColumn['default'] === 'true') {
$tableColumn['default'] = true;
}
if ($tableColumn['default'] === 'false') {
$tableColumn['default'] = false;
}
$length = null;
break;
case 'json':
case 'text':
case '_varchar':
case 'varchar':
$tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
$fixed = false;
break;
case 'interval':
$fixed = false;
break;
case 'char':
case 'bpchar':
$fixed = true;
break;
case 'float':
case 'float4':
case 'float8':
case 'double':
case 'double precision':
case 'real':
case 'decimal':
case 'money':
case 'numeric':
$tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
if (
preg_match(
'([A-Za-z]+\(([0-9]+),([0-9]+)\))',
$tableColumn['complete_type'],
$match,
) === 1
) {
$precision = $match[1];
$scale = $match[2];
$length = null;
}
break;
case 'year':
$length = null;
break;
// PostgreSQL 9.4+ only
case 'jsonb':
$jsonb = true;
break;
}
if (
$tableColumn['default'] !== null && preg_match(
"('([^']+)'::)",
$tableColumn['default'],
$match,
) === 1
) {
$tableColumn['default'] = $match[1];
}
$options = [
'length' => $length,
'notnull' => (bool) $tableColumn['isnotnull'],
'default' => $tableColumn['default'],
'precision' => $precision,
'scale' => $scale,
'fixed' => $fixed,
'autoincrement' => $autoincrement,
'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
? $tableColumn['comment']
: null,
];
$column = new Column($tableColumn['field'], Type::getType($type), $options);
if (! empty($tableColumn['collation'])) {
$column->setPlatformOption('collation', $tableColumn['collation']);
}
if ($column->getType()->getName() === Types::JSON) {
if (! $column->getType() instanceof JsonType) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5049',
<<<'DEPRECATION'
%s not extending %s while being named %s is deprecated,
and will lead to jsonb never to being used in 4.0.,
DEPRECATION,
get_class($column->getType()),
JsonType::class,
Types::JSON,
);
}
$column->setPlatformOption('jsonb', $jsonb);
}
return $column;
}
/**
* PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
*
* @param mixed $defaultValue
*
* @return mixed
*/
private function fixVersion94NegativeNumericDefaultValue($defaultValue)
{
if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
return trim($defaultValue, '()');
}
return $defaultValue;
}
/**
* Parses a default value expression as given by PostgreSQL
*/
private function parseDefaultExpression(?string $default): ?string
{
if ($default === null) {
return $default;
}
return str_replace("''", "'", $default);
}
protected function selectTableNames(string $databaseName): Result
{
$sql = <<<'SQL'
SELECT quote_ident(table_name) AS table_name,
table_schema AS schema_name
FROM information_schema.tables
WHERE table_catalog = ?
AND table_schema NOT LIKE 'pg\_%'
AND table_schema != 'information_schema'
AND table_name != 'geometry_columns'
AND table_name != 'spatial_ref_sys'
AND table_type = 'BASE TABLE'
SQL;
return $this->_conn->executeQuery($sql, [$databaseName]);
}
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' c.relname AS table_name, n.nspname AS schema_name,';
}
$sql .= sprintf(<<<'SQL'
a.attnum,
quote_ident(a.attname) AS field,
t.typname AS type,
format_type(a.atttypid, a.atttypmod) AS complete_type,
(SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
(SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
(SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
a.attnotnull AS isnotnull,
(SELECT 't'
FROM pg_index
WHERE c.oid = pg_index.indrelid
AND pg_index.indkey[0] = a.attnum
AND pg_index.indisprimary = 't'
) AS pri,
(%s) AS default,
(SELECT pg_description.description
FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
) AS comment
FROM pg_attribute a
INNER JOIN pg_class c
ON c.oid = a.attrelid
INNER JOIN pg_type t
ON t.oid = a.atttypid
INNER JOIN pg_namespace n
ON n.oid = c.relnamespace
LEFT JOIN pg_depend d
ON d.objid = c.oid
AND d.deptype = 'e'
AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
SQL, $this->_platform->getDefaultColumnValueSQLSnippet());
$conditions = array_merge([
'a.attnum > 0',
"c.relkind = 'r'",
'd.refobjid IS NULL',
], $this->buildQueryConditions($tableName));
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum';
return $this->_conn->executeQuery($sql);
}
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
}
$sql .= <<<'SQL'
quote_ident(ic.relname) AS relname,
i.indisunique,
i.indisprimary,
i.indkey,
i.indrelid,
pg_get_expr(indpred, indrelid) AS "where"
FROM pg_index i
JOIN pg_class AS tc ON tc.oid = i.indrelid
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
JOIN pg_class AS ic ON ic.oid = i.indexrelid
WHERE ic.oid IN (
SELECT indexrelid
FROM pg_index i, pg_class c, pg_namespace n
SQL;
$conditions = array_merge([
'c.oid = i.indrelid',
'c.relnamespace = n.oid',
], $this->buildQueryConditions($tableName));
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ')';
return $this->_conn->executeQuery($sql);
}
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = 'SELECT';
if ($tableName === null) {
$sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
}
$sql .= <<<'SQL'
quote_ident(r.conname) as conname,
pg_get_constraintdef(r.oid, true) as condef
FROM pg_constraint r
JOIN pg_class AS tc ON tc.oid = r.conrelid
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
WHERE r.conrelid IN
(
SELECT c.oid
FROM pg_class c, pg_namespace n
SQL;
$conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName));
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'";
return $this->_conn->executeQuery($sql);
}
/**
* {@inheritDoc}
*/
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
{
$sql = <<<'SQL'
SELECT c.relname,
CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
obj_description(c.oid, 'pg_class') AS comment
FROM pg_class c
INNER JOIN pg_namespace n
ON n.oid = c.relnamespace
SQL;
$conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName));
$sql .= ' WHERE ' . implode(' AND ', $conditions);
return $this->_conn->fetchAllAssociativeIndexed($sql);
}
/**
* @param string|null $tableName
*
* @return list<string>
*/
private function buildQueryConditions($tableName): array
{
$conditions = [];
if ($tableName !== null) {
if (strpos($tableName, '.') !== false) {
[$schemaName, $tableName] = explode('.', $tableName);
$conditions[] = 'n.nspname = ' . $this->_platform->quoteStringLiteral($schemaName);
} else {
$conditions[] = 'n.nspname = ANY(current_schemas(false))';
}
$identifier = new Identifier($tableName);
$conditions[] = 'c.relname = ' . $this->_platform->quoteStringLiteral($identifier->getName());
}
$conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
return $conditions;
}
}
@@ -0,0 +1,788 @@
<?php
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\SQLite;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use function array_change_key_case;
use function array_map;
use function array_merge;
use function count;
use function explode;
use function file_exists;
use function implode;
use function preg_match;
use function preg_match_all;
use function preg_quote;
use function preg_replace;
use function rtrim;
use function str_replace;
use function strcasecmp;
use function strpos;
use function strtolower;
use function trim;
use function unlink;
use function usort;
use const CASE_LOWER;
/**
* Sqlite SchemaManager.
*
* @extends AbstractSchemaManager<SqlitePlatform>
*/
class SqliteSchemaManager extends AbstractSchemaManager
{
/**
* {@inheritDoc}
*/
public function listTableNames()
{
return $this->doListTableNames();
}
/**
* {@inheritDoc}
*/
public function listTables()
{
return $this->doListTables();
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see introspectTable()} instead.
*/
public function listTableDetails($name)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5595',
'%s is deprecated. Use introspectTable() instead.',
__METHOD__,
);
return $this->doListTableDetails($name);
}
/**
* {@inheritDoc}
*/
public function listTableColumns($table, $database = null)
{
return $this->doListTableColumns($table, $database);
}
/**
* {@inheritDoc}
*/
public function listTableIndexes($table)
{
return $this->doListTableIndexes($table);
}
/**
* {@inheritDoc}
*/
protected function fetchForeignKeyColumnsByTable(string $databaseName): array
{
$columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName);
if (count($columnsByTable) > 0) {
foreach ($columnsByTable as $table => $columns) {
$columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns);
}
}
return $columnsByTable;
}
/**
* {@inheritDoc}
*
* @deprecated Delete the database file using the filesystem.
*/
public function dropDatabase($database)
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4963',
'SqliteSchemaManager::dropDatabase() is deprecated. Delete the database file using the filesystem.',
);
if (! file_exists($database)) {
return;
}
unlink($database);
}
/**
* {@inheritDoc}
*
* @deprecated The engine will create the database file automatically.
*/
public function createDatabase($database)
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4963',
'SqliteSchemaManager::createDatabase() is deprecated.'
. ' The engine will create the database file automatically.',
);
$params = $this->_conn->getParams();
$params['path'] = $database;
unset($params['memory']);
$conn = DriverManager::getConnection($params);
$conn->connect();
$conn->close();
}
/**
* {@inheritDoc}
*/
public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
{
if (! $table instanceof Table) {
$table = $this->listTableDetails($table);
}
$this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [$foreignKey]));
}
/**
* {@inheritDoc}
*
* @deprecated Use {@see dropForeignKey()} and {@see createForeignKey()} instead.
*/
public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4897',
'SqliteSchemaManager::dropAndCreateForeignKey() is deprecated.'
. ' Use SqliteSchemaManager::dropForeignKey() and SqliteSchemaManager::createForeignKey() instead.',
);
if (! $table instanceof Table) {
$table = $this->listTableDetails($table);
}
$this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [$foreignKey]));
}
/**
* {@inheritDoc}
*/
public function dropForeignKey($foreignKey, $table)
{
if (! $table instanceof Table) {
$table = $this->listTableDetails($table);
}
$this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [], [$foreignKey]));
}
/**
* {@inheritDoc}
*/
public function listTableForeignKeys($table, $database = null)
{
$table = $this->normalizeName($table);
$columns = $this->selectForeignKeyColumns($database ?? 'main', $table)
->fetchAllAssociative();
if (count($columns) > 0) {
$columns = $this->addDetailsToTableForeignKeyColumns($table, $columns);
}
return $this->_getPortableTableForeignKeysList($columns);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableDefinition($table)
{
return $table['table_name'];
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
{
$indexBuffer = [];
// fetch primary
$indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]);
usort(
$indexArray,
/**
* @param array<string,mixed> $a
* @param array<string,mixed> $b
*/
static function (array $a, array $b): int {
if ($a['pk'] === $b['pk']) {
return $a['cid'] - $b['cid'];
}
return $a['pk'] - $b['pk'];
},
);
foreach ($indexArray as $indexColumnRow) {
if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') {
continue;
}
$indexBuffer[] = [
'key_name' => 'primary',
'primary' => true,
'non_unique' => false,
'column_name' => $indexColumnRow['name'],
];
}
// fetch regular indexes
foreach ($tableIndexes as $tableIndex) {
// Ignore indexes with reserved names, e.g. autoindexes
if (strpos($tableIndex['name'], 'sqlite_') === 0) {
continue;
}
$keyName = $tableIndex['name'];
$idx = [];
$idx['key_name'] = $keyName;
$idx['primary'] = false;
$idx['non_unique'] = ! $tableIndex['unique'];
$indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]);
foreach ($indexArray as $indexColumnRow) {
$idx['column_name'] = $indexColumnRow['name'];
$indexBuffer[] = $idx;
}
}
return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableColumnList($table, $database, $tableColumns)
{
$list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
// find column with autoincrement
$autoincrementColumn = null;
$autoincrementCount = 0;
foreach ($tableColumns as $tableColumn) {
if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') {
continue;
}
$autoincrementCount++;
if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
continue;
}
$autoincrementColumn = $tableColumn['name'];
}
if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
foreach ($list as $column) {
if ($autoincrementColumn !== $column->getName()) {
continue;
}
$column->setAutoincrement(true);
}
}
// inspect column collation and comments
$createSql = $this->getCreateTableSQL($table);
foreach ($list as $columnName => $column) {
$type = $column->getType();
if ($type instanceof StringType || $type instanceof TextType) {
$column->setPlatformOption(
'collation',
$this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY',
);
}
$comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
if ($comment === null) {
continue;
}
$type = $this->extractDoctrineTypeFromComment($comment, '');
if ($type !== '') {
$column->setType(Type::getType($type));
$comment = $this->removeDoctrineTypeFromComment($comment, $type);
}
$column->setComment($comment);
}
return $list;
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableColumnDefinition($tableColumn)
{
$parts = explode('(', $tableColumn['type']);
$tableColumn['type'] = trim($parts[0]);
if (isset($parts[1])) {
$length = trim($parts[1], ')');
$tableColumn['length'] = $length;
}
$dbType = strtolower($tableColumn['type']);
$length = $tableColumn['length'] ?? null;
$unsigned = false;
if (strpos($dbType, ' unsigned') !== false) {
$dbType = str_replace(' unsigned', '', $dbType);
$unsigned = true;
}
$fixed = false;
$type = $this->_platform->getDoctrineTypeMapping($dbType);
$default = $tableColumn['dflt_value'];
if ($default === 'NULL') {
$default = null;
}
if ($default !== null) {
// SQLite returns the default value as a literal expression, so we need to parse it
if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
$default = str_replace("''", "'", $matches[1]);
}
}
$notnull = (bool) $tableColumn['notnull'];
if (! isset($tableColumn['name'])) {
$tableColumn['name'] = '';
}
$precision = null;
$scale = null;
switch ($dbType) {
case 'char':
$fixed = true;
break;
case 'float':
case 'double':
case 'real':
case 'decimal':
case 'numeric':
if (isset($tableColumn['length'])) {
if (strpos($tableColumn['length'], ',') === false) {
$tableColumn['length'] .= ',0';
}
[$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
}
$length = null;
break;
}
$options = [
'length' => $length,
'unsigned' => $unsigned,
'fixed' => $fixed,
'notnull' => $notnull,
'default' => $default,
'precision' => $precision,
'scale' => $scale,
];
return new Column($tableColumn['name'], Type::getType($type), $options);
}
/**
* {@inheritDoc}
*/
protected function _getPortableViewDefinition($view)
{
return new View($view['name'], $view['sql']);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeysList($tableForeignKeys)
{
$list = [];
foreach ($tableForeignKeys as $value) {
$value = array_change_key_case($value, CASE_LOWER);
$id = $value['id'];
if (! isset($list[$id])) {
if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
$value['on_delete'] = null;
}
if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
$value['on_update'] = null;
}
$list[$id] = [
'name' => $value['constraint_name'],
'local' => [],
'foreign' => [],
'foreignTable' => $value['table'],
'onDelete' => $value['on_delete'],
'onUpdate' => $value['on_update'],
'deferrable' => $value['deferrable'],
'deferred' => $value['deferred'],
];
}
$list[$id]['local'][] = $value['from'];
if ($value['to'] === null) {
// Inferring a shorthand form for the foreign key constraint, where the "to" field is empty.
// @see https://www.sqlite.org/foreignkeys.html#fk_indexes.
$foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']);
if (! isset($foreignTableIndexes['primary'])) {
continue;
}
$list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()];
continue;
}
$list[$id]['foreign'][] = $value['to'];
}
return parent::_getPortableTableForeignKeysList($list);
}
/**
* {@inheritDoc}
*/
protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint
{
return new ForeignKeyConstraint(
$tableForeignKey['local'],
$tableForeignKey['foreignTable'],
$tableForeignKey['foreign'],
$tableForeignKey['name'],
[
'onDelete' => $tableForeignKey['onDelete'],
'onUpdate' => $tableForeignKey['onUpdate'],
'deferrable' => $tableForeignKey['deferrable'],
'deferred' => $tableForeignKey['deferred'],
],
);
}
private function parseColumnCollationFromSQL(string $column, string $sql): ?string
{
$pattern = '{(?:\W' . preg_quote($column) . '\W|\W'
. preg_quote($this->_platform->quoteSingleIdentifier($column))
. '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
if (preg_match($pattern, $sql, $match) !== 1) {
return null;
}
return $match[1];
}
private function parseTableCommentFromSQL(string $table, string $sql): ?string
{
$pattern = '/\s* # Allow whitespace characters at start of line
CREATE\sTABLE # Match "CREATE TABLE"
(?:\W"' . preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
. '\W) # Match table name (quoted and unquoted)
( # Start capture
(?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
)/ix';
if (preg_match($pattern, $sql, $match) !== 1) {
return null;
}
$comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
return $comment === '' ? null : $comment;
}
private function parseColumnCommentFromSQL(string $column, string $sql): ?string
{
$pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
. '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
if (preg_match($pattern, $sql, $match) !== 1) {
return null;
}
$comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
return $comment === '' ? null : $comment;
}
/** @throws Exception */
private function getCreateTableSQL(string $table): string
{
$sql = $this->_conn->fetchOne(
<<<'SQL'
SELECT sql
FROM (
SELECT *
FROM sqlite_master
UNION ALL
SELECT *
FROM sqlite_temp_master
)
WHERE type = 'table'
AND name = ?
SQL
,
[$table],
);
if ($sql !== false) {
return $sql;
}
return '';
}
/**
* @param list<array<string,mixed>> $columns
*
* @return list<array<string,mixed>>
*
* @throws Exception
*/
private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array
{
$foreignKeyDetails = $this->getForeignKeyDetails($table);
$foreignKeyCount = count($foreignKeyDetails);
foreach ($columns as $i => $column) {
// SQLite identifies foreign keys in reverse order of appearance in SQL
$columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]);
}
return $columns;
}
/**
* @param string $table
*
* @return list<array<string, mixed>>
*
* @throws Exception
*/
private function getForeignKeyDetails($table)
{
$createSql = $this->getCreateTableSQL($table);
if (
preg_match_all(
'#
(?:CONSTRAINT\s+(\S+)\s+)?
(?:FOREIGN\s+KEY[^)]+\)\s*)?
REFERENCES\s+\S+\s*(?:\([^)]+\))?
(?:
[^,]*?
(NOT\s+DEFERRABLE|DEFERRABLE)
(?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
)?#isx',
$createSql,
$match,
) === 0
) {
return [];
}
$names = $match[1];
$deferrable = $match[2];
$deferred = $match[3];
$details = [];
for ($i = 0, $count = count($match[0]); $i < $count; $i++) {
$details[] = [
'constraint_name' => isset($names[$i]) && $names[$i] !== '' ? $names[$i] : null,
'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0,
'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0,
];
}
return $details;
}
public function createComparator(): Comparator
{
return new SQLite\Comparator($this->_platform);
}
/**
* {@inheritDoc}
*
* @deprecated
*/
public function getSchemaSearchPaths()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4821',
'SqliteSchemaManager::getSchemaSearchPaths() is deprecated.',
);
// SQLite does not support schemas or databases
return [];
}
protected function selectTableNames(string $databaseName): Result
{
$sql = <<<'SQL'
SELECT name AS table_name
FROM sqlite_master
WHERE type = 'table'
AND name != 'sqlite_sequence'
AND name != 'geometry_columns'
AND name != 'spatial_ref_sys'
UNION ALL
SELECT name
FROM sqlite_temp_master
WHERE type = 'table'
ORDER BY name
SQL;
return $this->_conn->executeQuery($sql);
}
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = <<<'SQL'
SELECT t.name AS table_name,
c.*
FROM sqlite_master t
JOIN pragma_table_info(t.name) c
SQL;
$conditions = [
"t.type = 'table'",
"t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
];
$params = [];
if ($tableName !== null) {
$conditions[] = 't.name = ?';
$params[] = str_replace('.', '__', $tableName);
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = <<<'SQL'
SELECT t.name AS table_name,
i.*
FROM sqlite_master t
JOIN pragma_index_list(t.name) i
SQL;
$conditions = [
"t.type = 'table'",
"t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
];
$params = [];
if ($tableName !== null) {
$conditions[] = 't.name = ?';
$params[] = str_replace('.', '__', $tableName);
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq';
return $this->_conn->executeQuery($sql, $params);
}
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
{
$sql = <<<'SQL'
SELECT t.name AS table_name,
p.*
FROM sqlite_master t
JOIN pragma_foreign_key_list(t.name) p
ON p."seq" != '-1'
SQL;
$conditions = [
"t.type = 'table'",
"t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
];
$params = [];
if ($tableName !== null) {
$conditions[] = 't.name = ?';
$params[] = str_replace('.', '__', $tableName);
}
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq';
return $this->_conn->executeQuery($sql, $params);
}
/**
* {@inheritDoc}
*/
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
{
if ($tableName === null) {
$tables = $this->listTableNames();
} else {
$tables = [$tableName];
}
$tableOptions = [];
foreach ($tables as $table) {
$comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table));
if ($comment === null) {
continue;
}
$tableOptions[$table]['comment'] = $comment;
}
return $tableOptions;
}
}
@@ -0,0 +1,219 @@
<?php
namespace Doctrine\DBAL\Tools\Console\Command;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\Keywords\DB2Keywords;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MariaDb102Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQL57Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQL80Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQL84Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
use Doctrine\DBAL\Platforms\Keywords\OracleKeywords;
use Doctrine\DBAL\Platforms\Keywords\PostgreSQL100Keywords;
use Doctrine\DBAL\Platforms\Keywords\PostgreSQL94Keywords;
use Doctrine\DBAL\Platforms\Keywords\ReservedKeywordsValidator;
use Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords;
use Doctrine\DBAL\Platforms\Keywords\SQLServer2012Keywords;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function array_keys;
use function assert;
use function count;
use function implode;
use function is_array;
use function is_string;
/** @deprecated Use database documentation instead. */
class ReservedWordsCommand extends Command
{
use CommandCompatibility;
/** @var array<string,KeywordList> */
private array $keywordLists;
private ConnectionProvider $connectionProvider;
public function __construct(ConnectionProvider $connectionProvider)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5431',
'ReservedWordsCommand is deprecated. Use database documentation instead.',
);
parent::__construct();
$this->connectionProvider = $connectionProvider;
$this->keywordLists = [
'db2' => new DB2Keywords(),
'mariadb102' => new MariaDb102Keywords(),
'mysql' => new MySQLKeywords(),
'mysql57' => new MySQL57Keywords(),
'mysql80' => new MySQL80Keywords(),
'mysql84' => new MySQL84Keywords(),
'oracle' => new OracleKeywords(),
'pgsql' => new PostgreSQL94Keywords(),
'pgsql100' => new PostgreSQL100Keywords(),
'sqlite' => new SQLiteKeywords(),
'sqlserver' => new SQLServer2012Keywords(),
];
}
/**
* Add or replace a keyword list.
*/
public function setKeywordList(string $name, KeywordList $keywordList): void
{
$this->keywordLists[$name] = $keywordList;
}
/**
* If you want to add or replace a keywords list use this command.
*
* @param string $name
* @param class-string<KeywordList> $class
*
* @return void
*/
public function setKeywordListClass($name, $class)
{
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4510',
'ReservedWordsCommand::setKeywordListClass() is deprecated,'
. ' use ReservedWordsCommand::setKeywordList() instead.',
);
$this->keywordLists[$name] = new $class();
}
/** @return void */
protected function configure()
{
$this
->setName('dbal:reserved-words')
->setDescription('Checks if the current database contains identifiers that are reserved.')
->setDefinition([
new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'),
new InputOption(
'list',
'l',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Keyword-List name.',
),
])
->setHelp(<<<'EOT'
Checks if the current database contains tables and columns
with names that are identifiers in this dialect or in other SQL dialects.
By default all supported platform keywords are checked:
<info>%command.full_name%</info>
If you want to check against specific dialects you can
pass them to the command:
<info>%command.full_name% -l mysql -l pgsql</info>
The following keyword lists are currently shipped with Doctrine:
* db2
* mariadb102
* mysql
* mysql57
* mysql80
* mysql84
* oracle
* pgsql
* pgsql100
* sqlite
* sqlserver
EOT);
}
/** @throws Exception */
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$output->writeln(
'<comment>The <info>dbal:reserved-words</info> command is deprecated.</comment>'
. ' Use the documentation on the used database platform(s) instead.',
);
$output->writeln('');
$conn = $this->getConnection($input);
$keywordLists = $input->getOption('list');
if (is_string($keywordLists)) {
$keywordLists = [$keywordLists];
} elseif (! is_array($keywordLists)) {
$keywordLists = [];
}
if (count($keywordLists) === 0) {
$keywordLists = array_keys($this->keywordLists);
}
$keywords = [];
foreach ($keywordLists as $keywordList) {
if (! isset($this->keywordLists[$keywordList])) {
throw new InvalidArgumentException(
"There exists no keyword list with name '" . $keywordList . "'. " .
'Known lists: ' . implode(', ', array_keys($this->keywordLists)),
);
}
$keywords[] = $this->keywordLists[$keywordList];
}
$output->write(
'Checking keyword violations for <comment>' . implode(', ', $keywordLists) . '</comment>...',
true,
);
$schema = $conn->getSchemaManager()->introspectSchema();
$visitor = new ReservedKeywordsValidator($keywords);
$schema->visit($visitor);
$violations = $visitor->getViolations();
if (count($violations) !== 0) {
$output->write(
'There are <error>' . count($violations) . '</error> reserved keyword violations'
. ' in your database schema:',
true,
);
foreach ($violations as $violation) {
$output->write(' - ' . $violation, true);
}
return 1;
}
$output->write('No reserved keywords violations have been found!', true);
return 0;
}
private function getConnection(InputInterface $input): Connection
{
$connectionName = $input->getOption('connection');
assert(is_string($connectionName) || $connectionName === null);
if ($connectionName !== null) {
return $this->connectionProvider->getConnection($connectionName);
}
return $this->connectionProvider->getDefaultConnection();
}
}
@@ -0,0 +1,122 @@
<?php
namespace Doctrine\DBAL\Types;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use function get_class;
/**
* DateTime type accepting additional information about timezone offsets.
*
* Caution: Databases are not necessarily experts at storing timezone related
* data of dates. First, of not all the supported vendors support storing Timezone data, and some of
* them only use the offset to calculate the timestamp in its default timezone (usually UTC) and persist
* the value without the offset information. They even don't save the actual timezone names attached
* to a DateTime instance (for example "Europe/Berlin" or "America/Montreal") but the current offset
* of them related to UTC. That means, depending on daylight saving times or not, you may get different
* offsets.
*
* This datatype makes only sense to use, if your application only needs to accept the timezone offset,
* not the actual timezone that uses transitions. Otherwise your DateTime instance
* attached with a timezone such as "Europe/Berlin" gets saved into the database with
* the offset and re-created from persistence with only the offset, not the original timezone
* attached.
*/
class DateTimeTzType extends Type implements PhpDateTimeMappingType
{
/**
* {@inheritDoc}
*/
public function getName()
{
return Types::DATETIMETZ_MUTABLE;
}
/**
* {@inheritDoc}
*/
public function getSQLDeclaration(array $column, AbstractPlatform $platform)
{
return $platform->getDateTimeTzTypeDeclarationSQL($column);
}
/**
* {@inheritDoc}
*
* @param T $value
*
* @return (T is null ? null : string)
*
* @template T
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return $value;
}
if ($value instanceof DateTimeImmutable) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6017',
'Passing an instance of %s is deprecated, use %s::%s() instead.',
get_class($value),
DateTimeTzImmutableType::class,
__FUNCTION__,
);
}
if ($value instanceof DateTimeInterface) {
return $value->format($platform->getDateTimeTzFormatString());
}
throw ConversionException::conversionFailedInvalidType(
$value,
$this->getName(),
['null', DateTime::class],
);
}
/**
* {@inheritDoc}
*
* @param T $value
*
* @return (T is null ? null : DateTimeInterface)
*
* @template T
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value instanceof DateTimeImmutable) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6017',
'Passing an instance of %s is deprecated, use %s::%s() instead.',
get_class($value),
DateTimeTzImmutableType::class,
__FUNCTION__,
);
}
if ($value === null || $value instanceof DateTimeInterface) {
return $value;
}
$dateTime = DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value);
if ($dateTime !== false) {
return $dateTime;
}
throw ConversionException::conversionFailedFormat(
$value,
$this->getName(),
$platform->getDateTimeTzFormatString(),
);
}
}
@@ -0,0 +1,218 @@
# Doctrine Deprecations
A small (side-effect free by default) layer on top of
`trigger_error(E_USER_DEPRECATED)` or PSR-3 logging.
- no side-effects by default, making it a perfect fit for libraries that don't know how the error handler works they operate under
- options to avoid having to rely on error handlers global state by using PSR-3 logging
- deduplicate deprecation messages to avoid excessive triggering and reduce overhead
We recommend to collect Deprecations using a PSR logger instead of relying on
the global error handler.
## Usage from consumer perspective:
Enable Doctrine deprecations to be sent to a PSR3 logger:
```php
\Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
```
Enable Doctrine deprecations to be sent as `@trigger_error($message, E_USER_DEPRECATED)`
messages by setting the `DOCTRINE_DEPRECATIONS` environment variable to `trigger`.
Alternatively, call:
```php
\Doctrine\Deprecations\Deprecation::enableWithTriggerError();
```
If you only want to enable deprecation tracking, without logging or calling `trigger_error`
then set the `DOCTRINE_DEPRECATIONS` environment variable to `track`.
Alternatively, call:
```php
\Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
```
Tracking is enabled with all three modes and provides access to all triggered
deprecations and their individual count:
```php
$deprecations = \Doctrine\Deprecations\Deprecation::getTriggeredDeprecations();
foreach ($deprecations as $identifier => $count) {
echo $identifier . " was triggered " . $count . " times\n";
}
```
### Suppressing Specific Deprecations
Disable triggering about specific deprecations:
```php
\Doctrine\Deprecations\Deprecation::ignoreDeprecations("https://link/to/deprecations-description-identifier");
```
Disable all deprecations from a package
```php
\Doctrine\Deprecations\Deprecation::ignorePackage("doctrine/orm");
```
### Other Operations
When used within PHPUnit or other tools that could collect multiple instances of the same deprecations
the deduplication can be disabled:
```php
\Doctrine\Deprecations\Deprecation::withoutDeduplication();
```
Disable deprecation tracking again:
```php
\Doctrine\Deprecations\Deprecation::disable();
```
## Usage from a library/producer perspective:
When you want to unconditionally trigger a deprecation even when called
from the library itself then the `trigger` method is the way to go:
```php
\Doctrine\Deprecations\Deprecation::trigger(
"doctrine/orm",
"https://link/to/deprecations-description",
"message"
);
```
If variable arguments are provided at the end, they are used with `sprintf` on
the message.
```php
\Doctrine\Deprecations\Deprecation::trigger(
"doctrine/orm",
"https://github.com/doctrine/orm/issue/1234",
"message %s %d",
"foo",
1234
);
```
When you want to trigger a deprecation only when it is called by a function
outside of the current package, but not trigger when the package itself is the cause,
then use:
```php
\Doctrine\Deprecations\Deprecation::triggerIfCalledFromOutside(
"doctrine/orm",
"https://link/to/deprecations-description",
"message"
);
```
Based on the issue link each deprecation message is only triggered once per
request.
A limited stacktrace is included in the deprecation message to find the
offending location.
Note: A producer/library should never call `Deprecation::enableWith` methods
and leave the decision how to handle deprecations to application and
frameworks.
## Usage in PHPUnit tests
There is a `VerifyDeprecations` trait that you can use to make assertions on
the occurrence of deprecations within a test.
```php
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
class MyTest extends TestCase
{
use VerifyDeprecations;
public function testSomethingDeprecation()
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');
triggerTheCodeWithDeprecation();
}
public function testSomethingDeprecationFixed()
{
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');
triggerTheCodeWithoutDeprecation();
}
}
```
## Displaying deprecations after running a PHPUnit test suite
It is possible to integrate this library with PHPUnit to display all
deprecations triggered during the test suite execution.
```xml
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
displayDetailsOnTestsThatTriggerDeprecations="true"
failOnDeprecation="true"
>
<!-- one attribute to display the deprecations, the other to fail the test suite -->
<php>
<!-- ensures native PHP deprecations are used -->
<server name="DOCTRINE_DEPRECATIONS" value="trigger"/>
</php>
<!-- ensures the @ operator in @trigger_error is ignored -->
<source ignoreSuppressionOfDeprecations="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
```
Note that you can still trigger Deprecations in your code, provided you use the
`#[WithoutErrorHandler]` attribute to disable PHPUnit's error handler for tests
that call it. Be wary that this will disable all error handling, meaning it
will mask any warnings or errors that would otherwise be caught by PHPUnit.
At the moment, it is not possible to disable deduplication with an environment
variable, but you can use a bootstrap file to achieve that:
```php
// tests/bootstrap.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use Doctrine\Deprecations\Deprecation;
Deprecation::withoutDeduplication();
```
Then, reference that file in your PHPUnit configuration:
```xml
<phpunit
bootstrap="tests/bootstrap.php"
>
</phpunit>
```
## What is a deprecation identifier?
An identifier for deprecations is just a link to any resource, most often a
Github Issue or Pull Request explaining the deprecation and potentially its
alternative.
@@ -0,0 +1,36 @@
{
"name": "doctrine/deprecations",
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"license": "MIT",
"type": "library",
"homepage": "https://www.doctrine-project.org/",
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12",
"phpstan/phpstan": "1.4.10 || 2.0.3",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"DeprecationTests\\": "test_fixtures/src",
"Doctrine\\Foo\\": "test_fixtures/vendor/doctrine/foo"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
@@ -0,0 +1,68 @@
{
"name": "doctrine/event-manager",
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
"license": "MIT",
"type": "library",
"keywords": [
"events",
"event",
"event dispatcher",
"event manager",
"event system"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"require": {
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^12",
"phpstan/phpstan": "^1.8.8",
"phpunit/phpunit": "^10.5",
"vimeo/psalm": "^5.24"
},
"conflict": {
"doctrine/common": "<2.9"
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\Common\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
}
}
@@ -0,0 +1,131 @@
PHP Cron Expression Parser
==========================
[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Tests](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml/badge.svg)](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337)
The PHP cron expression parser can parse a CRON expression, determine if it is
due to run, calculate the next run date of the expression, and calculate the previous
run date of the expression. You can calculate dates far into the future or past by
skipping **n** number of matching dates.
The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9),
lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to
find the last day of the month, **L** to find the last given weekday of a month, and hash
(#) to find the nth weekday of a given month.
More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork.
Installing
==========
Add the dependency to your project:
```bash
composer require dragonmantank/cron-expression
```
Usage
=====
```php
<?php
require_once '/vendor/autoload.php';
// Works with predefined scheduling definitions
$cron = new Cron\CronExpression('@daily');
$cron->isDue();
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');
// Works with complex expressions
$cron = new Cron\CronExpression('3-59/15 6-12 */15 1 2-5');
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
// Calculate a run date two iterations into the future
$cron = new Cron\CronExpression('@daily');
echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
// Calculate a run date relative to a specific time
$cron = new Cron\CronExpression('@monthly');
echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s');
```
CRON Expressions
================
A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows:
```
* * * * *
- - - - -
| | | | |
| | | | |
| | | | +----- day of week (0-7) (Sunday = 0 or 7) (or SUN-SAT)
| | | +--------- month (1-12) (or JAN-DEC)
| | +------------- day of month (1-31)
| +----------------- hour (0-23)
+--------------------- minute (0-59)
```
Each part of expression can also use wildcard, lists, ranges and steps:
- wildcard - match always
- `* * * * *` - At every minute.
- day of week and day of month also support `?`, an alias to `*`
- lists - match list of values, ranges and steps
- e.g. `15,30 * * * *` - At minute 15 and 30.
- ranges - match values in range
- e.g. `1-9 * * * *` - At every minute from 1 through 9.
- steps - match every nth value in range
- e.g. `*/5 * * * *` - At every 5th minute.
- e.g. `0-30/5 * * * *` - At every 5th minute from 0 through 30.
- combinations
- e.g. `0-14,30-44 * * * *` - At every minute from 0 through 14 and every minute from 30 through 44.
You can also use macro instead of an expression:
- `@yearly`, `@annually` - At 00:00 on 1st of January. (same as `0 0 1 1 *`)
- `@monthly` - At 00:00 on day-of-month 1. (same as `0 0 1 * *`)
- `@weekly` - At 00:00 on Sunday. (same as `0 0 * * 0`)
- `@daily`, `@midnight` - At 00:00. (same as `0 0 * * *`)
- `@hourly` - At minute 0. (same as `0 * * * *`)
Day of month extra features:
- nearest weekday - weekday (Monday-Friday) nearest to the given day
- e.g. `* * 15W * *` - At every minute on a weekday nearest to the 15th.
- If you were to specify `15W` as the value, the meaning is: "the nearest weekday to the 15th of the month"
So if the 15th is a Saturday, the trigger will fire on Friday the 14th.
If the 15th is a Sunday, the trigger will fire on Monday the 16th.
If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
- However, if you specify `1W` as the value for day-of-month,
and the 1st is a Saturday, the trigger will fire on Monday the 3rd,
as it will not 'jump' over the boundary of a month's days.
- last day of the month
- e.g. `* * L * *` - At every minute on a last day-of-month.
- last weekday of the month
- e.g. `* * LW * *` - At every minute on a last weekday.
Day of week extra features:
- nth day
- e.g. `* * * * 7#4` - At every minute on 4th Sunday.
- 1-5
- Every day of week repeats 4-5 times a month. To target the last one, use "last day" feature instead.
- last day
- e.g. `* * * * 7L` - At every minute on the last Sunday.
Requirements
============
- PHP 7.2+
- PHPUnit is required to run the unit tests
- Composer is required to run the unit tests
Projects that Use cron-expression
=================================
* Part of the [Laravel Framework](https://github.com/laravel/framework/)
* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle)
* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/crunzphp/crunz)
* Framework agnostic job scheduler - with locks, parallelism, per-second scheduling and more - [orisai/scheduler](https://github.com/orisai/scheduler)
* Explain expression in English (and other languages) with [orisai/cron-expression-explainer](https://github.com/orisai/cron-expression-explainer)
@@ -0,0 +1,51 @@
{
"name": "dragonmantank/cron-expression",
"type": "library",
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": ["cron", "schedule"],
"license": "MIT",
"authors": [
{
"name": "Chris Tankersley",
"email": "chris@ctankersley.com",
"homepage": "https://github.com/dragonmantank"
}
],
"require": {
"php": "^7.2|^8.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0",
"phpstan/extension-installer": "^1.0"
},
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"autoload-dev": {
"psr-4": {
"Cron\\Tests\\": "tests/Cron/"
}
},
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"scripts": {
"phpstan": "./vendor/bin/phpstan analyze",
"test": "phpunit"
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"phpstan/extension-installer": true
}
}
}
@@ -0,0 +1,591 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
/**
* CRON expression parser that can determine whether or not a CRON expression is
* due to run, the next run date and previous run date of a CRON expression.
* The determinations made by this class are accurate if checked run once per
* minute (seconds are dropped from date time comparisons).
*
* Schedule parts must map to:
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
* [1-7|MON-SUN], and an optional year.
*
* @see http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
public const MINUTE = 0;
public const HOUR = 1;
public const DAY = 2;
public const MONTH = 3;
public const WEEKDAY = 4;
/** @deprecated */
public const YEAR = 5;
public const MAPPINGS = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@midnight' => '0 0 * * *',
'@hourly' => '0 * * * *',
];
/**
* @var array CRON expression parts
*/
protected $cronParts;
/**
* @var FieldFactoryInterface CRON field factory
*/
protected $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
protected $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
protected static $order = [
self::YEAR,
self::MONTH,
self::DAY,
self::WEEKDAY,
self::HOUR,
self::MINUTE,
];
/**
* @var array<string, string>
*/
private static $registeredAliases = self::MAPPINGS;
/**
* Registered a user defined CRON Expression Alias.
*
* @throws LogicException If the expression or the alias name are invalid
* or if the alias is already registered.
*/
public static function registerAlias(string $alias, string $expression): void
{
try {
new self($expression);
} catch (InvalidArgumentException $exception) {
throw new LogicException("The expression `$expression` is invalid", 0, $exception);
}
$shortcut = strtolower($alias);
if (1 !== preg_match('/^@\w+$/', $shortcut)) {
throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}
if (isset(self::$registeredAliases[$shortcut])) {
throw new LogicException("The alias `$alias` is already registered.");
}
self::$registeredAliases[$shortcut] = $expression;
}
/**
* Unregistered a user defined CRON Expression Alias.
*
* @throws LogicException If the user tries to unregister a built-in alias
*/
public static function unregisterAlias(string $alias): bool
{
$shortcut = strtolower($alias);
if (isset(self::MAPPINGS[$shortcut])) {
throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered.");
}
if (!isset(self::$registeredAliases[$shortcut])) {
return false;
}
unset(self::$registeredAliases[$shortcut]);
return true;
}
/**
* Tells whether a CRON Expression alias is registered.
*/
public static function supportsAlias(string $alias): bool
{
return isset(self::$registeredAliases[strtolower($alias)]);
}
/**
* Returns all registered aliases as an associated array where the aliases are the key
* and their associated expressions are the values.
*
* @return array<string, string>
*/
public static function getAliases(): array
{
return self::$registeredAliases;
}
/**
* @deprecated since version 3.0.2, use __construct instead.
*/
public static function factory(string $expression, ?FieldFactoryInterface $fieldFactory = null): CronExpression
{
/** @phpstan-ignore-next-line */
return new static($expression, $fieldFactory);
}
/**
* Validate a CronExpression.
*
* @param string $expression the CRON expression to validate
*
* @return bool True if a valid CRON expression was passed. False if not.
*/
public static function isValidExpression(string $expression): bool
{
try {
new CronExpression($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Parse a CRON expression.
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields
* @throws InvalidArgumentException
*/
public function __construct(string $expression, ?FieldFactoryInterface $fieldFactory = null)
{
$shortcut = strtolower($expression);
$expression = self::$registeredAliases[$shortcut] ?? $expression;
$this->fieldFactory = $fieldFactory ?: new FieldFactory();
$this->setExpression($expression);
}
/**
* Set or change the CRON expression.
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @throws \InvalidArgumentException if not a valid CRON expression
*
* @return CronExpression
*/
public function setExpression(string $value): CronExpression
{
$split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (!\is_array($split)) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
$notEnoughParts = \count($split) < 5;
$questionMarkInInvalidPart = array_key_exists(0, $split) && $split[0] === '?'
|| array_key_exists(1, $split) && $split[1] === '?'
|| array_key_exists(3, $split) && $split[3] === '?';
$tooManyQuestionMarks = array_key_exists(2, $split) && $split[2] === '?'
&& array_key_exists(4, $split) && $split[4] === '?';
if ($notEnoughParts || $questionMarkInInvalidPart || $tooManyQuestionMarks) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
$this->cronParts = $split;
foreach ($this->cronParts as $position => $part) {
$this->setPart($position, $part);
}
return $this;
}
/**
* Set part of the CRON expression.
*
* @param int $position The position of the CRON expression to set
* @param string $value The value to set
*
* @throws \InvalidArgumentException if the value is not valid for the part
*
* @return CronExpression
*/
public function setPart(int $position, string $value): CronExpression
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
'Invalid CRON field value ' . $value . ' at position ' . $position
);
}
$this->cronParts[$position] = $value;
return $this;
}
/**
* Set max iteration count for searching next run dates.
*
* @param int $maxIterationCount Max iteration count when searching for next run date
*
* @return CronExpression
*/
public function setMaxIterationCount(int $maxIterationCount): CronExpression
{
$this->maxIterationCount = $maxIterationCount;
return $this;
}
/**
* Get a next run date relative to the current date or a specific date
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning a
* matching next run date. 0, the default, will return the
* current date and time if the next run date falls on the
* current date and time. Setting this value to 1 will
* skip the first match and go to the second match.
* Setting this value to 2 will skip the first 2
* matches and so on.
* @param bool $allowCurrentDate Set to TRUE to return the current date if
* it matches the cron expression.
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*/
public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
/**
* Get a previous run date relative to the current date or a specific date.
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
}
/**
* Get multiple run dates starting at the current date or a specific date.
*
* @param int $total Set the total number of dates to calculate
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param bool $invert Set to TRUE to retrieve previous dates
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @return \DateTime[] Returns an array of run dates
*/
public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
if (!$currentTime instanceof DateTime) {
throw new InvalidArgumentException('invalid current time');
}
$currentTime->setTimezone(new DateTimeZone($timeZone));
$matches = [];
for ($i = 0; $i < $total; ++$i) {
try {
$result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
$allowCurrentDate = false;
$currentTime = clone $result;
$matches[] = $result;
}
return $matches;
}
/**
* Get all or part of the CRON expression.
*
* @param int|string|null $part specify the part to retrieve or NULL to get the full
* cron schedule string
*
* @return null|string Returns the CRON expression, a part of the
* CRON expression, or NULL if the part was specified but not found
*/
public function getExpression($part = null): ?string
{
if (null === $part) {
return implode(' ', $this->cronParts);
}
if (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Gets the parts of the cron expression as an array.
*
* @return string[]
* The array of parts that make up this expression.
*/
public function getParts()
{
return $this->cronParts;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString(): string
{
return (string) $this->getExpression();
}
/**
* Determine if the cron is due to run based on the current date or a
* specific date. This method assumes that the current number of
* seconds are irrelevant, and should be called once per minute.
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @return bool Returns TRUE if the cron is due to run or FALSE if not
*/
public function isDue($currentTime = 'now', $timeZone = null): bool
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
if (!$currentTime instanceof DateTime) {
throw new InvalidArgumentException('invalid current time');
}
$currentTime->setTimezone(new DateTimeZone($timeZone));
// drop the seconds to 0
$currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);
try {
return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
} catch (Exception $e) {
return false;
}
}
/**
* Get the next or previous run date of the expression relative to a date.
*
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $invert Set to TRUE to go backwards in time
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param string|null $timeZone TimeZone to use instead of the system default
*
* @throws \RuntimeException on too many iterations
* @throws Exception
*
* @return \DateTime
*/
protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentDate = new DateTime($currentTime);
} else {
$currentDate = new DateTime('now');
}
if (!$currentDate instanceof DateTime) {
throw new InvalidArgumentException('invalid current date');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
// Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074
$currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone());
if ($currentDate === false) {
throw new \RuntimeException('Unable to create date from format');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
$nextRun = clone $currentDate;
// We don't have to satisfy * or null fields
$parts = [];
$fields = [];
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) {
$domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3));
$dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4));
$domExpression = new self($domExpression);
$dowExpression = new self($dowExpression);
$domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') {
$domRunDates = [];
}
if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') {
$dowRunDates = [];
}
$combined = array_merge($domRunDates, $dowRunDates);
usort($combined, function ($a, $b) {
return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s');
});
if ($invert) {
$combined = array_reverse($combined);
}
return $combined[$nth];
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; ++$i) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (false === strpos($part, ',')) {
$satisfied = $field->isSatisfiedBy($nextRun, $part, $invert);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
/**
* Workout what timeZone should be used.
*
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param string|null $timeZone TimeZone to use instead of the system default
*
* @return string
*/
protected function determineTimeZone($currentTime, ?string $timeZone): string
{
if (null !== $timeZone) {
return $timeZone;
}
if ($currentTime instanceof DateTimeInterface) {
return $currentTime->getTimezone()->getName();
}
return date_default_timezone_get();
}
}
@@ -0,0 +1,329 @@
<?php
namespace Egulias\EmailValidator;
use Doctrine\Common\Lexer\AbstractLexer;
use Doctrine\Common\Lexer\Token;
/** @extends AbstractLexer<int, string> */
class EmailLexer extends AbstractLexer
{
//ASCII values
public const S_EMPTY = -1;
public const C_NUL = 0;
public const S_HTAB = 9;
public const S_LF = 10;
public const S_CR = 13;
public const S_SP = 32;
public const EXCLAMATION = 33;
public const S_DQUOTE = 34;
public const NUMBER_SIGN = 35;
public const DOLLAR = 36;
public const PERCENTAGE = 37;
public const AMPERSAND = 38;
public const S_SQUOTE = 39;
public const S_OPENPARENTHESIS = 40;
public const S_CLOSEPARENTHESIS = 41;
public const ASTERISK = 42;
public const S_PLUS = 43;
public const S_COMMA = 44;
public const S_HYPHEN = 45;
public const S_DOT = 46;
public const S_SLASH = 47;
public const S_COLON = 58;
public const S_SEMICOLON = 59;
public const S_LOWERTHAN = 60;
public const S_EQUAL = 61;
public const S_GREATERTHAN = 62;
public const QUESTIONMARK = 63;
public const S_AT = 64;
public const S_OPENBRACKET = 91;
public const S_BACKSLASH = 92;
public const S_CLOSEBRACKET = 93;
public const CARET = 94;
public const S_UNDERSCORE = 95;
public const S_BACKTICK = 96;
public const S_OPENCURLYBRACES = 123;
public const S_PIPE = 124;
public const S_CLOSECURLYBRACES = 125;
public const S_TILDE = 126;
public const C_DEL = 127;
public const INVERT_QUESTIONMARK = 168;
public const INVERT_EXCLAMATION = 173;
public const GENERIC = 300;
public const S_IPV6TAG = 301;
public const INVALID = 302;
public const CRLF = 1310;
public const S_DOUBLECOLON = 5858;
public const ASCII_INVALID_FROM = 127;
public const ASCII_INVALID_TO = 199;
/**
* US-ASCII visible characters not valid for atext (@link http://tools.ietf.org/html/rfc5322#section-3.2.3)
*
* @var array<string, int>
*/
protected $charValue = [
'{' => self::S_OPENCURLYBRACES,
'}' => self::S_CLOSECURLYBRACES,
'(' => self::S_OPENPARENTHESIS,
')' => self::S_CLOSEPARENTHESIS,
'<' => self::S_LOWERTHAN,
'>' => self::S_GREATERTHAN,
'[' => self::S_OPENBRACKET,
']' => self::S_CLOSEBRACKET,
':' => self::S_COLON,
';' => self::S_SEMICOLON,
'@' => self::S_AT,
'\\' => self::S_BACKSLASH,
'/' => self::S_SLASH,
',' => self::S_COMMA,
'.' => self::S_DOT,
"'" => self::S_SQUOTE,
"`" => self::S_BACKTICK,
'"' => self::S_DQUOTE,
'-' => self::S_HYPHEN,
'::' => self::S_DOUBLECOLON,
' ' => self::S_SP,
"\t" => self::S_HTAB,
"\r" => self::S_CR,
"\n" => self::S_LF,
"\r\n" => self::CRLF,
'IPv6' => self::S_IPV6TAG,
'' => self::S_EMPTY,
'\0' => self::C_NUL,
'*' => self::ASTERISK,
'!' => self::EXCLAMATION,
'&' => self::AMPERSAND,
'^' => self::CARET,
'$' => self::DOLLAR,
'%' => self::PERCENTAGE,
'~' => self::S_TILDE,
'|' => self::S_PIPE,
'_' => self::S_UNDERSCORE,
'=' => self::S_EQUAL,
'+' => self::S_PLUS,
'¿' => self::INVERT_QUESTIONMARK,
'?' => self::QUESTIONMARK,
'#' => self::NUMBER_SIGN,
'¡' => self::INVERT_EXCLAMATION,
];
public const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu";
public const VALID_UTF8_REGEX = '/\p{Cc}+/u';
public const CATCHABLE_PATTERNS = [
'[a-zA-Z]+[46]?', //ASCII and domain literal
'[^\x00-\x7F]', //UTF-8
'[0-9]+',
'\r\n',
'::',
'\s+?',
'.',
];
public const NON_CATCHABLE_PATTERNS = [
'[\xA0-\xff]+',
];
public const MODIFIERS = 'iu';
/** @var bool */
protected $hasInvalidTokens = false;
/**
* @var Token<int, string>
*/
protected Token $previous;
/**
* The last matched/seen token.
*
* @var Token<int, string>
*/
public Token $current;
/**
* @var Token<int, string>
*/
private Token $nullToken;
/** @var string */
private $accumulator = '';
/** @var bool */
private $hasToRecord = false;
public function __construct()
{
/** @var Token<int, string> $nullToken */
$nullToken = new Token('', self::S_EMPTY, 0);
$this->nullToken = $nullToken;
$this->current = $this->previous = $this->nullToken;
$this->lookahead = null;
}
public function reset(): void
{
$this->hasInvalidTokens = false;
parent::reset();
$this->current = $this->previous = $this->nullToken;
}
/**
* @param int $type
* @throws \UnexpectedValueException
* @return boolean
*
*/
public function find($type): bool
{
$search = clone $this;
$search->skipUntil($type);
if (!$search->lookahead) {
throw new \UnexpectedValueException($type . ' not found');
}
return true;
}
/**
* moveNext
*
* @return boolean
*/
public function moveNext(): bool
{
if ($this->hasToRecord && $this->previous === $this->nullToken) {
$this->accumulator .= $this->current->value;
}
$this->previous = $this->current;
if ($this->lookahead === null) {
$this->lookahead = $this->nullToken;
}
$hasNext = parent::moveNext();
$this->current = $this->token ?? $this->nullToken;
if ($this->hasToRecord) {
$this->accumulator .= $this->current->value;
}
return $hasNext;
}
/**
* Retrieve token type. Also processes the token value if necessary.
*
* @param string $value
* @throws \InvalidArgumentException
* @return integer
*/
protected function getType(&$value): int
{
$encoded = $value;
if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') {
$encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252');
}
if ($this->isValid($encoded)) {
return $this->charValue[$encoded];
}
if ($this->isNullType($encoded)) {
return self::C_NUL;
}
if ($this->isInvalidChar($encoded)) {
$this->hasInvalidTokens = true;
return self::INVALID;
}
return self::GENERIC;
}
protected function isValid(string $value): bool
{
return isset($this->charValue[$value]);
}
protected function isNullType(string $value): bool
{
return $value === "\0";
}
protected function isInvalidChar(string $value): bool
{
return !preg_match(self::INVALID_CHARS_REGEX, $value);
}
protected function isUTF8Invalid(string $value): bool
{
return preg_match(self::VALID_UTF8_REGEX, $value) !== false;
}
public function hasInvalidTokens(): bool
{
return $this->hasInvalidTokens;
}
/**
* getPrevious
*
* @return Token<int, string>
*/
public function getPrevious(): Token
{
return $this->previous;
}
/**
* Lexical catchable patterns.
*
* @return string[]
*/
protected function getCatchablePatterns(): array
{
return self::CATCHABLE_PATTERNS;
}
/**
* Lexical non-catchable patterns.
*
* @return string[]
*/
protected function getNonCatchablePatterns(): array
{
return self::NON_CATCHABLE_PATTERNS;
}
protected function getModifiers(): string
{
return self::MODIFIERS;
}
public function getAccumulatedValues(): string
{
return $this->accumulator;
}
public function startRecording(): void
{
$this->hasToRecord = true;
}
public function stopRecording(): void
{
$this->hasToRecord = false;
}
public function clearRecorded(): void
{
$this->accumulator = '';
}
}
@@ -0,0 +1,38 @@
<?php
namespace Egulias\EmailValidator\Parser\CommentStrategy;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Warning\CFWSNearAt;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Warning\Warning;
class LocalComment implements CommentStrategy
{
/**
* @var array<int, Warning>
*/
private $warnings = [];
public function exitCondition(EmailLexer $lexer, int $openedParenthesis): bool
{
return !$lexer->isNextToken(EmailLexer::S_AT);
}
public function endOfLoopValidations(EmailLexer $lexer): Result
{
if (!$lexer->isNextToken(EmailLexer::S_AT)) {
return new InvalidEmail(new ExpectingATEXT('ATEX is not expected after closing comments'), $lexer->current->value);
}
$this->warnings[CFWSNearAt::CODE] = new CFWSNearAt();
return new ValidEmail();
}
public function getWarnings(): array
{
return $this->warnings;
}
}
@@ -0,0 +1,327 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Doctrine\Common\Lexer\Token;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Warning\TLD;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\DotAtEnd;
use Egulias\EmailValidator\Result\Reason\DotAtStart;
use Egulias\EmailValidator\Warning\DeprecatedComment;
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd;
use Egulias\EmailValidator\Result\Reason\LabelTooLong;
use Egulias\EmailValidator\Result\Reason\NoDomainPart;
use Egulias\EmailValidator\Result\Reason\ConsecutiveAt;
use Egulias\EmailValidator\Result\Reason\DomainTooLong;
use Egulias\EmailValidator\Result\Reason\CharNotAllowed;
use Egulias\EmailValidator\Result\Reason\DomainHyphened;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
class DomainPart extends PartParser
{
public const DOMAIN_MAX_LENGTH = 253;
public const LABEL_MAX_LENGTH = 63;
/**
* @var string
*/
protected $domainPart = '';
/**
* @var string
*/
protected $label = '';
public function parse(): Result
{
$this->lexer->clearRecorded();
$this->lexer->startRecording();
$this->lexer->moveNext();
$domainChecks = $this->performDomainStartChecks();
if ($domainChecks->isInvalid()) {
return $domainChecks;
}
if ($this->lexer->current->isA(EmailLexer::S_AT)) {
return new InvalidEmail(new ConsecutiveAt(), $this->lexer->current->value);
}
$result = $this->doParseDomainPart();
if ($result->isInvalid()) {
return $result;
}
$end = $this->checkEndOfDomain();
if ($end->isInvalid()) {
return $end;
}
$this->lexer->stopRecording();
$this->domainPart = $this->lexer->getAccumulatedValues();
$length = strlen($this->domainPart);
if ($length > self::DOMAIN_MAX_LENGTH) {
return new InvalidEmail(new DomainTooLong(), $this->lexer->current->value);
}
return new ValidEmail();
}
private function checkEndOfDomain(): Result
{
$prev = $this->lexer->getPrevious();
if ($prev->isA(EmailLexer::S_DOT)) {
return new InvalidEmail(new DotAtEnd(), $this->lexer->current->value);
}
if ($prev->isA(EmailLexer::S_HYPHEN)) {
return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev->value);
}
if ($this->lexer->current->isA(EmailLexer::S_SP)) {
return new InvalidEmail(new CRLFAtTheEnd(), $prev->value);
}
return new ValidEmail();
}
private function performDomainStartChecks(): Result
{
$invalidTokens = $this->checkInvalidTokensAfterAT();
if ($invalidTokens->isInvalid()) {
return $invalidTokens;
}
$missingDomain = $this->checkEmptyDomain();
if ($missingDomain->isInvalid()) {
return $missingDomain;
}
if ($this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS)) {
$this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
}
return new ValidEmail();
}
private function checkEmptyDomain(): Result
{
$thereIsNoDomain = $this->lexer->current->isA(EmailLexer::S_EMPTY) ||
($this->lexer->current->isA(EmailLexer::S_SP) &&
!$this->lexer->isNextToken(EmailLexer::GENERIC));
if ($thereIsNoDomain) {
return new InvalidEmail(new NoDomainPart(), $this->lexer->current->value);
}
return new ValidEmail();
}
private function checkInvalidTokensAfterAT(): Result
{
if ($this->lexer->current->isA(EmailLexer::S_DOT)) {
return new InvalidEmail(new DotAtStart(), $this->lexer->current->value);
}
if ($this->lexer->current->isA(EmailLexer::S_HYPHEN)) {
return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->current->value);
}
return new ValidEmail();
}
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new DomainComment());
$result = $commentParser->parse();
$this->warnings = [...$this->warnings, ...$commentParser->getWarnings()];
return $result;
}
protected function doParseDomainPart(): Result
{
$tldMissing = true;
$hasComments = false;
$domain = '';
do {
$prev = $this->lexer->getPrevious();
$notAllowedChars = $this->checkNotAllowedChars($this->lexer->current);
if ($notAllowedChars->isInvalid()) {
return $notAllowedChars;
}
if (
$this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS) ||
$this->lexer->current->isA(EmailLexer::S_CLOSEPARENTHESIS)
) {
$hasComments = true;
$commentsResult = $this->parseComments();
//Invalid comment parsing
if ($commentsResult->isInvalid()) {
return $commentsResult;
}
}
$dotsResult = $this->checkConsecutiveDots();
if ($dotsResult->isInvalid()) {
return $dotsResult;
}
if ($this->lexer->current->isA(EmailLexer::S_OPENBRACKET)) {
$literalResult = $this->parseDomainLiteral();
$this->addTLDWarnings($tldMissing);
return $literalResult;
}
$labelCheck = $this->checkLabelLength();
if ($labelCheck->isInvalid()) {
return $labelCheck;
}
$FwsResult = $this->parseFWS();
if ($FwsResult->isInvalid()) {
return $FwsResult;
}
$domain .= $this->lexer->current->value;
if ($this->lexer->current->isA(EmailLexer::S_DOT) && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
$tldMissing = false;
}
$exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments);
if ($exceptionsResult->isInvalid()) {
return $exceptionsResult;
}
$this->lexer->moveNext();
} while (!$this->lexer->current->isA(EmailLexer::S_EMPTY));
$labelCheck = $this->checkLabelLength(true);
if ($labelCheck->isInvalid()) {
return $labelCheck;
}
$this->addTLDWarnings($tldMissing);
$this->domainPart = $domain;
return new ValidEmail();
}
/**
* @param Token<int, string> $token
*
* @return Result
*/
private function checkNotAllowedChars(Token $token): Result
{
$notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH => true];
if (isset($notAllowed[$token->type])) {
return new InvalidEmail(new CharNotAllowed(), $token->value);
}
return new ValidEmail();
}
/**
* @return Result
*/
protected function parseDomainLiteral(): Result
{
try {
$this->lexer->find(EmailLexer::S_CLOSEBRACKET);
} catch (\RuntimeException $e) {
return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->current->value);
}
$domainLiteralParser = new DomainLiteralParser($this->lexer);
$result = $domainLiteralParser->parse();
$this->warnings = [...$this->warnings, ...$domainLiteralParser->getWarnings()];
return $result;
}
/**
* @param Token<int, string> $prev
* @param bool $hasComments
*
* @return Result
*/
protected function checkDomainPartExceptions(Token $prev, bool $hasComments): Result
{
if ($this->lexer->current->isA(EmailLexer::S_OPENBRACKET) && $prev->type !== EmailLexer::S_AT) {
return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->current->value);
}
if ($this->lexer->current->isA(EmailLexer::S_HYPHEN) && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->current->value);
}
if (
$this->lexer->current->isA(EmailLexer::S_BACKSLASH)
&& $this->lexer->isNextToken(EmailLexer::GENERIC)
) {
return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->current->value);
}
return $this->validateTokens($hasComments);
}
protected function validateTokens(bool $hasComments): Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if (!isset($validDomainTokens[$this->lexer->current->type])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->current->value), $this->lexer->current->value);
}
return new ValidEmail();
}
private function checkLabelLength(bool $isEndOfDomain = false): Result
{
if ($this->lexer->current->isA(EmailLexer::S_DOT) || $isEndOfDomain) {
if ($this->isLabelTooLong($this->label)) {
return new InvalidEmail(new LabelTooLong(), $this->lexer->current->value);
}
$this->label = '';
}
$this->label .= $this->lexer->current->value;
return new ValidEmail();
}
private function isLabelTooLong(string $label): bool
{
if (preg_match('/[^\x00-\x7F]/', $label)) {
idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
/** @psalm-var array{errors: int, ...} $idnaInfo */
return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
}
return strlen($label) > self::LABEL_MAX_LENGTH;
}
private function addTLDWarnings(bool $isTLDMissing): void
{
if ($isTLDMissing) {
$this->warnings[TLD::CODE] = new TLD();
}
}
public function domainPart(): string
{
return $this->domainPart;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Egulias\EmailValidator\Validation;
class DNSGetRecordWrapper
{
/**
* @param string $host
* @param int $type
*
* @return DNSRecords
*/
public function getRecords(string $host, int $type): DNSRecords
{
// A workaround to fix https://bugs.php.net/bug.php?id=73149
set_error_handler(
static function (int $errorLevel, string $errorMessage): never {
throw new \RuntimeException("Unable to get DNS record for the host: $errorMessage");
}
);
try {
// Get all MX, A and AAAA DNS records for host
return new DNSRecords(dns_get_record($host, $type));
} catch (\RuntimeException $exception) {
return new DNSRecords([], true);
} finally {
restore_error_handler();
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Egulias\EmailValidator\Validation;
class DNSRecords
{
/**
* @param list<array<array-key, mixed>> $records
* @param bool $error
*/
public function __construct(private readonly array $records, private readonly bool $error = false)
{
}
/**
* @return list<array<array-key, mixed>>
*/
public function getRecords(): array
{
return $this->records;
}
public function withError(): bool
{
return $this->error;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Egulias\EmailValidator\Validation\Extra;
use \Spoofchecker;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\SpoofEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Validation\EmailValidation;
class SpoofCheckValidation implements EmailValidation
{
/**
* @var InvalidEmail|null
*/
private $error;
public function __construct()
{
if (!extension_loaded('intl')) {
throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
}
}
public function isValid(string $email, EmailLexer $emailLexer) : bool
{
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::SINGLE_SCRIPT);
if ($checker->isSuspicious($email)) {
$this->error = new SpoofEmail();
}
return $this->error === null;
}
public function getError() : ?InvalidEmail
{
return $this->error;
}
public function getWarnings() : array
{
return [];
}
}
@@ -0,0 +1,27 @@
<?php
namespace Egulias\EmailValidator\Warning;
use UnitEnum;
class QuotedPart extends Warning
{
public const CODE = 36;
/**
* @param UnitEnum|string|int|null $prevToken
* @param UnitEnum|string|int|null $postToken
*/
public function __construct($prevToken, $postToken)
{
if ($prevToken instanceof UnitEnum) {
$prevToken = $prevToken->name;
}
if ($postToken instanceof UnitEnum) {
$postToken = $postToken->name;
}
$this->message = "Deprecated Quoted String found between $prevToken and $postToken";
}
}
@@ -0,0 +1,17 @@
<?php
namespace Egulias\EmailValidator\Warning;
class QuotedString extends Warning
{
public const CODE = 11;
/**
* @param string|int $prevToken
* @param string|int $postToken
*/
public function __construct($prevToken, $postToken)
{
$this->message = "Quoted String found between $prevToken and $postToken";
}
}
+209
View File
@@ -0,0 +1,209 @@
# CHANGELOG
## [Unreleased](https://github.com/FakerPHP/Faker/compare/v1.24.0...1.24.1)
- Removed domain `gmail.com.au` from `Provider\en_AU\Internet` (#886)
## [2024-11-09, v1.24.0](https://github.com/FakerPHP/Faker/compare/v1.23.1..v1.24.0)
- Fix internal deprecations in Doctrine's populator by @gnutix in (#889)
- Fix mobile phone number pattern for France by @ker0x in (#859)
- PHP 8.4 Support by @Jubeki in (#904)
- Added support for PHP 8.4 (#904)
## [2023-09-29, v1.23.1](https://github.com/FakerPHP/Faker/compare/v1.23.0..v1.23.1)
- Fixed double `а` female lastName in `ru_RU/Person::name()` (#832)
- Fixed polish license plates (#685)
- Stopped using `static` in callables in `Provider\pt_BR\PhoneNumber` (#785)
- Fixed incorrect female name (#794)
- Stopped using the deprecated `MT_RAND_PHP` constant to seed the random generator on PHP 8.3 (#844)
## [2023-06-12, v1.23.0](https://github.com/FakerPHP/Faker/compare/v1.22.0..v1.23.0)
- Update `randomElements` to return random number of elements when no count is provided (#658)
## [2023-05-14, v1.22.0](https://github.com/FakerPHP/Faker/compare/v1.21.0..v1.22.0)
- Fixed `randomElements()` to accept empty iterator (#605)
- Added support for passing an `Enum` to `randomElement()` and `randomElements()` (#620)
- Started rejecting invalid arguments passed to `randomElement()` and `randomElements()` (#642)
## [2022-12-13, v1.21.0](https://github.com/FakerPHP/Faker/compare/v1.20.0..v1.21.0)
- Dropped support for PHP 7.1, 7.2, and 7.3 (#543)
- Added support for PHP 8.2 (#528)
## [2022-07-20, v1.20.0](https://github.com/FakerPHP/Faker/compare/v1.19.0..v1.20.0)
- Fixed typo in French phone number (#452)
- Fixed some Hungarian naming bugs (#451)
- Fixed bug where the NL-BE VAT generation was incorrect (#455)
- Improve Turkish phone numbers for E164 and added landline support (#460)
- Add Microsoft Edge User Agent (#464)
- Added option to set image formats on Faker\Provider\Image (#473)
- Added support for French color translations (#466)
- Support filtering timezones by country code (#480)
- Fixed typo in some greek names (#490)
- Marked the Faker\Provider\Image as deprecated
## [2022-02-02, v1.19.0](https://github.com/FakerPHP/Faker/compare/v1.18.0..v1.19.0)
- Added color extension to core (#442)
- Added conflict with `doctrine/persistence` below version `1.4`
- Fix for support on different Doctrine ORM versions (#414)
- Fix usage of `Doctrine\Persistence` dependency
- Fix CZ Person birthNumber docblock return type (#437)
- Fix is_IS Person docbock types (#439)
- Fix is_IS Address docbock type (#438)
- Fix regexify escape backslash in character class (#434)
- Removed UUID from Generator to be able to extend it (#441)
## [2022-01-23, v1.18.0](https://github.com/FakerPHP/Faker/compare/v1.17.0..v1.18.0)
- Deprecated UUID, use uuid3 to specify version (#427)
- Reset formatters when adding a new provider (#366)
- Helper methods to use our custom generators (#155)
- Set allow-plugins for Composer 2.2 (#405)
- Fix kk_KZ\Person::individualIdentificationNumber generation (#411)
- Allow for -> syntax to be used in parsing (#423)
- Person->name was missing string return type (#424)
- Generate a valid BE TAX number (#415)
- Added the UUID extension to Core (#427)
## [2021-12-05, v1.17.0](https://github.com/FakerPHP/Faker/compare/v1.16.0..v1.17.0)
- Partial PHP 8.1 compatibility (#373)
- Add payment provider for `ne_NP` locale (#375)
- Add Egyptian Arabic `ar_EG` locale (#377)
- Updated list of South African TLDs (#383)
- Fixed formatting of E.164 numbers (#380)
- Allow `symfony/deprecation-contracts` `^3.0` (#397)
## [2021-09-06, v1.16.0](https://github.com/FakerPHP/Faker/compare/v1.15.0..v1.16.0)
- Add Company extension
- Add Address extension
- Add Person extension
- Add PhoneNumber extension
- Add VersionExtension (#350)
- Stricter types in Extension\Container and Extension\GeneratorAwareExtension (#345)
- Fix deprecated property access in `nl_NL` (#348)
- Add support for `psr/container` >= 2.0 (#354)
- Add missing union types in Faker\Generator (#352)
## [2021-07-06, v1.15.0](https://github.com/FakerPHP/Faker/compare/v1.14.1..v1.15.0)
- Updated the generator phpdoc to help identify magic methods (#307)
- Prevent direct access and triggered deprecation warning for "word" (#302)
- Updated length on all global e164 numbers (#301)
- Updated last names from different source (#312)
- Don't generate birth number of '000' for Swedish personal identity (#306)
- Add job list for localization id_ID (#339)
## [2021-03-30, v1.14.1](https://github.com/FakerPHP/Faker/compare/v1.14.0..v1.14.1)
- Fix where randomNumber and randomFloat would return a 0 value (#291 / #292)
## [2021-03-29, v1.14.0](https://github.com/FakerPHP/Faker/compare/v1.13.0..v1.14.0)
- Fix for realText to ensure the text keeps closer to its boundaries (#152)
- Fix where regexify produces a random character instead of a literal dot (#135
- Deprecate zh_TW methods that only call base methods (#122)
- Add used extensions to composer.json as suggestion (#120)
- Moved TCNo and INN from calculator to localized providers (#108)
- Fix regex dot/backslash issue where a dot is replaced with a backslash as escape character (#206)
- Deprecate direct property access (#164)
- Added test to assert unique() behaviour (#233)
- Added RUC for the es_PE locale (#244)
- Test IBAN formats for Latin America (AR/PE/VE) (#260)
- Added VAT number for en_GB (#255)
- Added new districts for the ne_NP locale (#258)
- Fix for U.S. Area Code Generation (#261)
- Fix in numerify where a better random numeric value is guaranteed (#256)
- Fix e164PhoneNumber to only generate valid phone numbers with valid country codes (#264)
- Extract fixtures into separate classes (#234)
- Remove french domains that no longer exists (#277)
- Fix error that occurs when getting a polish title (#279)
- Use valid area codes for North America E164 phone numbers (#280)
- Adding support for extensions and PSR-11 (#154)
- Adding trait for GeneratorAwareExtension (#165)
- Added helper class for extension (#162)
- Added blood extension to core (#232)
- Added barcode extension to core (#252)
- Added number extension (#257)
- Various code style updates
- Added a note about our breaking change promise (#273)
## [2020-12-18, v1.13.0](https://github.com/FakerPHP/Faker/compare/v1.12.1..v1.13.0)
Several fixes and new additions in this release. A lot of cleanup has been done
on the codebase on both tests and consistency.
- Feature/pl pl license plate (#62)
- Fix greek phone numbers (#16)
- Move AT payment provider logic to de_AT (#72)
- Fix wiktionary links (#73)
- Fix AT person links (#74)
- Fix AT cities (#75)
- Deprecate at_AT providers (#78)
- Add Austrian `ssn()` to `Person` provider (#79)
- Fix typos in id_ID Address (#83)
- Austrian post codes (#86)
- Updated Polish data (#70)
- Improve Austrian social security number generation (#88)
- Move US phone numbers with extension to own method (#91)
- Add UK National Insurance number generator (#89)
- Fix en_SG phone number generator (#100)
- Remove usage of mt_rand (#87)
- Remove whitespace from beginning of el_GR phone numbers (#105)
- Building numbers can not be 0, 00, 000 (#107)
- Add 172.16/12 local IPv4 block (#121)
- Add JCB credit card type (#124)
- Remove json_decode from emoji generation (#123)
- Remove ro street address (#146)
## [2020-12-11, v1.12.1](https://github.com/FakerPHP/Faker/compare/v1.12.0..v1.12.1)
This is a security release that prevents a hacker to execute code on the server.
## [2020-11-23, v1.12.0](https://github.com/FakerPHP/Faker/compare/v1.11.0..v1.12.0)
- Fix ro_RO first and last day of year calculation offset (#65)
- Fix en_NG locale test namespaces that did not match PSR-4 (#57)
- Added Singapore NRIC/FIN provider (#56)
- Added provider for Lithuanian municipalities (#58)
- Added blood types provider (#61)
## [2020-11-15, v1.11.0](https://github.com/FakerPHP/Faker/compare/v1.10.1..v1.11.0)
- Added Provider for Swedish Municipalities
- Updates to person names in pt_BR
- Many code style changes
## [2020-10-28, v1.10.1](https://github.com/FakerPHP/Faker/compare/v1.10.0..v1.10.1)
- Updates the Danish addresses in dk_DK
- Removed offense company names in nl_NL
- Clarify changelog with original fork
- Standin replacement for LoremPixel to Placeholder.com (#11)
## [2020-10-27, v1.10.0](https://github.com/FakerPHP/Faker/compare/v1.9.1..v1.10.0)
- Support PHP 7.1-8.0
- Fix typo in de_DE Company Provider
- Fix dateTimeThisYear method
- Fix typo in de_DE jobTitleFormat
- Fix IBAN generation for CR
- Fix typos in greek first names
- Fix US job title typo
- Do not clear entity manager for doctrine orm populator
- Remove persian rude words
- Corrections to RU names
## 2020-10-27, v1.9.1
- Initial version. Same as `fzaninotto/Faker:v1.9.1`.
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Calculator;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Barcode implements Extension\BarcodeExtension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
private function ean(int $length = 13): string
{
$code = Extension\Helper::numerify(str_repeat('#', $length - 1));
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
}
public function ean13(): string
{
return $this->ean();
}
public function ean8(): string
{
return $this->ean(8);
}
public function isbn10(): string
{
$code = Extension\Helper::numerify(str_repeat('#', 9));
return sprintf('%s%s', $code, Calculator\Isbn::checksum($code));
}
public function isbn13(): string
{
$code = '97' . $this->numberExtension->numberBetween(8, 9) . Extension\Helper::numerify(str_repeat('#', 9));
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
}
}
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
use Faker\Extension\Helper;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Color implements Extension\ColorExtension
{
private Extension\NumberExtension $numberExtension;
/**
* @var string[]
*/
private array $safeColorNames = [
'black', 'maroon', 'green', 'navy', 'olive',
'purple', 'teal', 'lime', 'blue', 'silver',
'gray', 'yellow', 'fuchsia', 'aqua', 'white',
];
/**
* @var string[]
*/
private array $allColorNames = [
'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine',
'Azure', 'Beige', 'Bisque', 'Black', 'BlanchedAlmond',
'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue',
'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue',
'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan',
'DarkGoldenRod', 'DarkGray', 'DarkGreen', 'DarkKhaki',
'DarkMagenta', 'DarkOliveGreen', 'Darkorange', 'DarkOrchid',
'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue',
'DarkSlateGray', 'DarkTurquoise', 'DarkViolet', 'DeepPink',
'DeepSkyBlue', 'DimGray', 'DimGrey', 'DodgerBlue', 'FireBrick',
'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite',
'Gold', 'GoldenRod', 'Gray', 'Green', 'GreenYellow', 'HoneyDew',
'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender',
'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral',
'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGreen', 'LightPink',
'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSteelBlue',
'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquaMarine',
'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue',
'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue',
'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive',
'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen',
'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum',
'PowderBlue', 'Purple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon',
'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue',
'SlateGray', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato',
'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen',
];
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* @example '#fa3cc2'
*/
public function hexColor(): string
{
return '#' . str_pad(dechex($this->numberExtension->numberBetween(1, 16777215)), 6, '0', STR_PAD_LEFT);
}
/**
* @example '#ff0044'
*/
public function safeHexColor(): string
{
$color = str_pad(dechex($this->numberExtension->numberBetween(0, 255)), 3, '0', STR_PAD_LEFT);
return sprintf(
'#%s%s%s%s%s%s',
$color[0],
$color[0],
$color[1],
$color[1],
$color[2],
$color[2],
);
}
/**
* @example 'array(0,255,122)'
*
* @return int[]
*/
public function rgbColorAsArray(): array
{
$color = $this->hexColor();
return [
hexdec(substr($color, 1, 2)),
hexdec(substr($color, 3, 2)),
hexdec(substr($color, 5, 2)),
];
}
/**
* @example '0,255,122'
*/
public function rgbColor(): string
{
return implode(',', $this->rgbColorAsArray());
}
/**
* @example 'rgb(0,255,122)'
*/
public function rgbCssColor(): string
{
return sprintf(
'rgb(%s)',
$this->rgbColor(),
);
}
/**
* @example 'rgba(0,255,122,0.8)'
*/
public function rgbaCssColor(): string
{
return sprintf(
'rgba(%s,%s)',
$this->rgbColor(),
$this->numberExtension->randomFloat(1, 0, 1),
);
}
/**
* @example 'blue'
*/
public function safeColorName(): string
{
return Helper::randomElement($this->safeColorNames);
}
/**
* @example 'NavajoWhite'
*/
public function colorName(): string
{
return Helper::randomElement($this->allColorNames);
}
/**
* @example '340,50,20'
*/
public function hslColor(): string
{
return sprintf(
'%s,%s,%s',
$this->numberExtension->numberBetween(0, 360),
$this->numberExtension->numberBetween(0, 100),
$this->numberExtension->numberBetween(0, 100),
);
}
/**
* @example array(340, 50, 20)
*
* @return int[]
*/
public function hslColorAsArray(): array
{
return [
$this->numberExtension->numberBetween(0, 360),
$this->numberExtension->numberBetween(0, 100),
$this->numberExtension->numberBetween(0, 100),
];
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Coordinates implements Extension\Extension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* @example '77.147489'
*
* @return float Uses signed degrees format (returns a float number between -90 and 90)
*/
public function latitude(float $min = -90.0, float $max = 90.0): float
{
if ($min < -90 || $max < -90) {
throw new \LogicException('Latitude cannot be less that -90.0');
}
if ($min > 90 || $max > 90) {
throw new \LogicException('Latitude cannot be greater that 90.0');
}
return $this->randomFloat(6, $min, $max);
}
/**
* @example '86.211205'
*
* @return float Uses signed degrees format (returns a float number between -180 and 180)
*/
public function longitude(float $min = -180.0, float $max = 180.0): float
{
if ($min < -180 || $max < -180) {
throw new \LogicException('Longitude cannot be less that -180.0');
}
if ($min > 180 || $max > 180) {
throw new \LogicException('Longitude cannot be greater that 180.0');
}
return $this->randomFloat(6, $min, $max);
}
/**
* @example array('77.147489', '86.211205')
*
* @return array{latitude: float, longitude: float}
*/
public function localCoordinates(): array
{
return [
'latitude' => $this->latitude(),
'longitude' => $this->longitude(),
];
}
private function randomFloat(int $nbMaxDecimals, float $min, float $max): float
{
if ($min > $max) {
throw new \LogicException('Invalid coordinates boundaries');
}
return $this->numberExtension->randomFloat($nbMaxDecimals, $min, $max);
}
}
@@ -0,0 +1,217 @@
<?php
namespace Faker\Core;
use Faker\Extension\DateTimeExtension;
use Faker\Extension\GeneratorAwareExtension;
use Faker\Extension\GeneratorAwareExtensionTrait;
use Faker\Extension\Helper;
/**
* @experimental This class is experimental and does not fall under our BC promise
*
* @since 1.20.0
*/
final class DateTime implements DateTimeExtension, GeneratorAwareExtension
{
use GeneratorAwareExtensionTrait;
/**
* @var string[]
*/
private array $centuries = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI'];
private ?string $defaultTimezone = null;
/**
* Get the POSIX-timestamp of a DateTime, int or string.
*
* @param \DateTime|float|int|string $until
*
* @return false|int
*/
private function getTimestamp($until = 'now')
{
if (is_numeric($until)) {
return (int) $until;
}
if ($until instanceof \DateTime) {
return $until->getTimestamp();
}
return strtotime(empty($until) ? 'now' : $until);
}
/**
* Get a DateTime created based on a POSIX-timestamp.
*
* @param int $timestamp the UNIX / POSIX-compatible timestamp
*/
private function getTimestampDateTime(int $timestamp): \DateTime
{
return new \DateTime('@' . $timestamp);
}
private function resolveTimezone(?string $timezone): string
{
if ($timezone !== null) {
return $timezone;
}
return null === $this->defaultTimezone ? date_default_timezone_get() : $this->defaultTimezone;
}
/**
* Internal method to set the timezone on a DateTime object.
*/
private function setTimezone(\DateTime $dateTime, ?string $timezone): \DateTime
{
$timezone = $this->resolveTimezone($timezone);
return $dateTime->setTimezone(new \DateTimeZone($timezone));
}
public function dateTime($until = 'now', ?string $timezone = null): \DateTime
{
return $this->setTimezone(
$this->getTimestampDateTime($this->unixTime($until)),
$timezone,
);
}
public function dateTimeAD($until = 'now', ?string $timezone = null): \DateTime
{
$min = (PHP_INT_SIZE > 4) ? -62135597361 : -PHP_INT_MAX;
return $this->setTimezone(
$this->getTimestampDateTime($this->generator->numberBetween($min, $this->getTimestamp($until))),
$timezone,
);
}
public function dateTimeBetween($from = '-30 years', $until = 'now', ?string $timezone = null): \DateTime
{
$start = $this->getTimestamp($from);
$end = $this->getTimestamp($until);
if ($start > $end) {
throw new \InvalidArgumentException('"$from" must be anterior to "$until".');
}
$timestamp = $this->generator->numberBetween($start, $end);
return $this->setTimezone(
$this->getTimestampDateTime($timestamp),
$timezone,
);
}
public function dateTimeInInterval($from = '-30 years', string $interval = '+5 days', ?string $timezone = null): \DateTime
{
$intervalObject = \DateInterval::createFromDateString($interval);
$datetime = $from instanceof \DateTime ? $from : new \DateTime($from);
$other = (clone $datetime)->add($intervalObject);
$begin = min($datetime, $other);
$end = $datetime === $begin ? $other : $datetime;
return $this->dateTimeBetween($begin, $end, $timezone);
}
public function dateTimeThisWeek($until = 'sunday this week', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('monday this week', $until, $timezone);
}
public function dateTimeThisMonth($until = 'last day of this month', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('first day of this month', $until, $timezone);
}
public function dateTimeThisYear($until = 'last day of december', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('first day of january', $until, $timezone);
}
public function dateTimeThisDecade($until = 'now', ?string $timezone = null): \DateTime
{
$year = floor(date('Y') / 10) * 10;
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
}
public function dateTimeThisCentury($until = 'now', ?string $timezone = null): \DateTime
{
$year = floor(date('Y') / 100) * 100;
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
}
public function date(string $format = 'Y-m-d', $until = 'now'): string
{
return $this->dateTime($until)->format($format);
}
public function time(string $format = 'H:i:s', $until = 'now'): string
{
return $this->date($format, $until);
}
public function unixTime($until = 'now'): int
{
return $this->generator->numberBetween(0, $this->getTimestamp($until));
}
public function iso8601($until = 'now'): string
{
return $this->date(\DateTime::ISO8601, $until);
}
public function amPm($until = 'now'): string
{
return $this->date('a', $until);
}
public function dayOfMonth($until = 'now'): string
{
return $this->date('d', $until);
}
public function dayOfWeek($until = 'now'): string
{
return $this->date('l', $until);
}
public function month($until = 'now'): string
{
return $this->date('m', $until);
}
public function monthName($until = 'now'): string
{
return $this->date('F', $until);
}
public function year($until = 'now'): string
{
return $this->date('Y', $until);
}
public function century(): string
{
return Helper::randomElement($this->centuries);
}
public function timezone(?string $countryCode = null): string
{
if ($countryCode) {
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $countryCode);
} else {
$timezones = \DateTimeZone::listIdentifiers();
}
return Helper::randomElement($timezones);
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Number implements Extension\NumberExtension
{
public function numberBetween(int $min = 0, int $max = 2147483647): int
{
$int1 = min($min, $max);
$int2 = max($min, $max);
return mt_rand($int1, $int2);
}
public function randomDigit(): int
{
return $this->numberBetween(0, 9);
}
public function randomDigitNot(int $except): int
{
$result = $this->numberBetween(0, 8);
if ($result >= $except) {
++$result;
}
return $result;
}
public function randomDigitNotZero(): int
{
return $this->numberBetween(1, 9);
}
public function randomFloat(?int $nbMaxDecimals = null, float $min = 0, ?float $max = null): float
{
if (null === $nbMaxDecimals) {
$nbMaxDecimals = $this->randomDigit();
}
if (null === $max) {
$max = $this->randomNumber();
if ($min > $max) {
$max = $min;
}
}
if ($min > $max) {
$tmp = $min;
$min = $max;
$max = $tmp;
}
return round($min + $this->numberBetween() / mt_getrandmax() * ($max - $min), $nbMaxDecimals);
}
public function randomNumber(?int $nbDigits = null, bool $strict = false): int
{
if (null === $nbDigits) {
$nbDigits = $this->randomDigitNotZero();
}
$max = 10 ** $nbDigits - 1;
if ($max > mt_getrandmax()) {
throw new \InvalidArgumentException('randomNumber() can only generate numbers up to mt_getrandmax()');
}
if ($strict) {
return $this->numberBetween(10 ** ($nbDigits - 1), $max);
}
return $this->numberBetween(0, $max);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Uuid implements Extension\UuidExtension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
public function uuid3(): string
{
// fix for compatibility with 32bit architecture; each mt_rand call is restricted to 32bit
// two such calls will cause 64bits of randomness regardless of architecture
$seed = $this->numberExtension->numberBetween(0, 2147483647) . '#' . $this->numberExtension->numberBetween(0, 2147483647);
// Hash the seed and convert to a byte array
$val = md5($seed, true);
$byte = array_values(unpack('C16', $val));
// extract fields from byte array
$tLo = ($byte[0] << 24) | ($byte[1] << 16) | ($byte[2] << 8) | $byte[3];
$tMi = ($byte[4] << 8) | $byte[5];
$tHi = ($byte[6] << 8) | $byte[7];
$csLo = $byte[9];
$csHi = $byte[8] & 0x3f | (1 << 7);
// correct byte order for big edian architecture
if (pack('L', 0x6162797A) == pack('N', 0x6162797A)) {
$tLo = (($tLo & 0x000000ff) << 24) | (($tLo & 0x0000ff00) << 8)
| (($tLo & 0x00ff0000) >> 8) | (($tLo & 0xff000000) >> 24);
$tMi = (($tMi & 0x00ff) << 8) | (($tMi & 0xff00) >> 8);
$tHi = (($tHi & 0x00ff) << 8) | (($tHi & 0xff00) >> 8);
}
// apply version number
$tHi &= 0x0fff;
$tHi |= (3 << 12);
// cast to string
return sprintf(
'%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x',
$tLo,
$tMi,
$tHi,
$csHi,
$csLo,
$byte[10],
$byte[11],
$byte[12],
$byte[13],
$byte[14],
$byte[15],
);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
use Faker\Provider\DateTime;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Version implements Extension\VersionExtension
{
private Extension\NumberExtension $numberExtension;
/**
* @var string[]
*/
private array $semverCommonPreReleaseIdentifiers = ['alpha', 'beta', 'rc'];
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* Represents v2.0.0 of the semantic versioning: https://semver.org/spec/v2.0.0.html
*/
public function semver(bool $preRelease = false, bool $build = false): string
{
return sprintf(
'%d.%d.%d%s%s',
$this->numberExtension->numberBetween(0, 9),
$this->numberExtension->numberBetween(0, 99),
$this->numberExtension->numberBetween(0, 99),
$preRelease && $this->numberExtension->numberBetween(0, 1) === 1 ? '-' . $this->semverPreReleaseIdentifier() : '',
$build && $this->numberExtension->numberBetween(0, 1) === 1 ? '+' . $this->semverBuildIdentifier() : '',
);
}
/**
* Common pre-release identifier
*/
private function semverPreReleaseIdentifier(): string
{
$ident = Extension\Helper::randomElement($this->semverCommonPreReleaseIdentifiers);
if ($this->numberExtension->numberBetween(0, 1) !== 1) {
return $ident;
}
return $ident . '.' . $this->numberExtension->numberBetween(1, 99);
}
/**
* Common random build identifier
*/
private function semverBuildIdentifier(): string
{
if ($this->numberExtension->numberBetween(0, 1) === 1) {
// short git revision syntax: https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection
return substr(sha1(Extension\Helper::lexify('??????')), 0, 7);
}
// date syntax
return DateTime::date('YmdHis');
}
}
@@ -0,0 +1,242 @@
<?php
namespace Faker\Extension;
/**
* FakerPHP extension for Date-related randomization.
*
* Functions accepting a date string use the `strtotime()` function internally.
*
* @experimental
*
* @since 1.20.0
*/
interface DateTimeExtension
{
/**
* Get a DateTime object between January 1, 1970, and `$until` (defaults to "now").
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*
* @example DateTime('2005-08-16 20:39:21')
*/
public function dateTime($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object for a date between January 1, 0001, and now.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @example DateTime('1265-03-22 21:15:52')
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeAD($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object a random date between `$from` and `$until`.
* Accepts date strings that can be recognized by `strtotime()`.
*
* @param \DateTime|string $from defaults to 30 years ago
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeBetween($from = '-30 years', $until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object based on a random date between `$from` and an interval.
* Accepts date string that can be recognized by `strtotime()`.
*
* @param \DateTime|int|string $from defaults to 30 years ago
* @param string $interval defaults to 5 days after
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeInInterval($from = '-30 years', string $interval = '+5 days', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current week.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisWeek($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisMonth($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current year.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisYear($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current decade.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisDecade($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current century.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisCentury($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date string between January 1, 1970, and `$until`.
*
* @param string $format DateTime format
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see https://www.php.net/manual/en/datetime.format.php
*/
public function date(string $format = 'Y-m-d', $until = 'now'): string;
/**
* Get a time string (24h format by default).
*
* @param string $format DateTime format
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see https://www.php.net/manual/en/datetime.format.php
*/
public function time(string $format = 'H:i:s', $until = 'now'): string;
/**
* Get a UNIX (POSIX-compatible) timestamp between January 1, 1970, and `$until`.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*/
public function unixTime($until = 'now'): int;
/**
* Get a date string according to the ISO-8601 standard.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*/
public function iso8601($until = 'now'): string;
/**
* Get a string containing either "am" or "pm".
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example 'am'
*/
public function amPm($until = 'now'): string;
/**
* Get a localized random day of the month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '16'
*/
public function dayOfMonth($until = 'now'): string;
/**
* Get a localized random day of the week.
*
* Uses internal DateTime formatting, hence PHP's internal locale will be used (change using `setlocale()`).
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example 'Tuesday'
*
* @see setlocale
* @see https://www.php.net/manual/en/function.setlocale.php Set a different output language
*/
public function dayOfWeek($until = 'now'): string;
/**
* Get a random month (numbered).
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '7'
*/
public function month($until = 'now'): string;
/**
* Get a random month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see setlocale
* @see https://www.php.net/manual/en/function.setlocale.php Set a different output language
*
* @example 'September'
*/
public function monthName($until = 'now'): string;
/**
* Get a random year between 1970 and `$until`.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '1987'
*/
public function year($until = 'now'): string;
/**
* Get a random century, formatted as Roman numerals.
*
* @example 'XVII'
*/
public function century(): string;
/**
* Get a random timezone, uses `\DateTimeZone::listIdentifiers()` internally.
*
* @param string|null $countryCode two-letter ISO 3166-1 compatible country code
*
* @example 'Europe/Rome'
*/
public function timezone(?string $countryCode = null): string;
}
@@ -0,0 +1,985 @@
<?php
namespace Faker;
use Faker\Container\ContainerInterface;
/**
* @property string $citySuffix
*
* @method string citySuffix()
*
* @property string $streetSuffix
*
* @method string streetSuffix()
*
* @property string $buildingNumber
*
* @method string buildingNumber()
*
* @property string $city
*
* @method string city()
*
* @property string $streetName
*
* @method string streetName()
*
* @property string $streetAddress
*
* @method string streetAddress()
*
* @property string $postcode
*
* @method string postcode()
*
* @property string $address
*
* @method string address()
*
* @property string $country
*
* @method string country()
*
* @property float $latitude
*
* @method float latitude($min = -90, $max = 90)
*
* @property float $longitude
*
* @method float longitude($min = -180, $max = 180)
*
* @property float[] $localCoordinates
*
* @method float[] localCoordinates()
*
* @property int $randomDigitNotNull
*
* @method int randomDigitNotNull()
*
* @property mixed $passthrough
*
* @method mixed passthrough($value)
*
* @property string $randomLetter
*
* @method string randomLetter()
*
* @property string $randomAscii
*
* @method string randomAscii()
*
* @property array $randomElements
*
* @method array randomElements($array = ['a', 'b', 'c'], $count = 1, $allowDuplicates = false)
*
* @property mixed $randomElement
*
* @method mixed randomElement($array = ['a', 'b', 'c'])
*
* @property int|string|null $randomKey
*
* @method int|string|null randomKey($array = [])
*
* @property array|string $shuffle
*
* @method array|string shuffle($arg = '')
*
* @property array $shuffleArray
*
* @method array shuffleArray($array = [])
*
* @property string $shuffleString
*
* @method string shuffleString($string = '', $encoding = 'UTF-8')
*
* @property string $numerify
*
* @method string numerify($string = '###')
*
* @property string $lexify
*
* @method string lexify($string = '????')
*
* @property string $bothify
*
* @method string bothify($string = '## ??')
*
* @property string $asciify
*
* @method string asciify($string = '****')
*
* @property string $regexify
*
* @method string regexify($regex = '')
*
* @property string $toLower
*
* @method string toLower($string = '')
*
* @property string $toUpper
*
* @method string toUpper($string = '')
*
* @property int $biasedNumberBetween
*
* @method int biasedNumberBetween($min = 0, $max = 100, $function = 'sqrt')
*
* @property string $hexColor
*
* @method string hexColor()
*
* @property string $safeHexColor
*
* @method string safeHexColor()
*
* @property array $rgbColorAsArray
*
* @method array rgbColorAsArray()
*
* @property string $rgbColor
*
* @method string rgbColor()
*
* @property string $rgbCssColor
*
* @method string rgbCssColor()
*
* @property string $rgbaCssColor
*
* @method string rgbaCssColor()
*
* @property string $safeColorName
*
* @method string safeColorName()
*
* @property string $colorName
*
* @method string colorName()
*
* @property string $hslColor
*
* @method string hslColor()
*
* @property array $hslColorAsArray
*
* @method array hslColorAsArray()
*
* @property string $company
*
* @method string company()
*
* @property string $companySuffix
*
* @method string companySuffix()
*
* @property string $jobTitle
*
* @method string jobTitle()
*
* @property int $unixTime
*
* @method int unixTime($max = 'now')
*
* @property \DateTime $dateTime
*
* @method \DateTime dateTime($max = 'now', $timezone = null)
*
* @property \DateTime $dateTimeAD
*
* @method \DateTime dateTimeAD($max = 'now', $timezone = null)
*
* @property string $iso8601
*
* @method string iso8601($max = 'now')
*
* @property string $date
*
* @method string date($format = 'Y-m-d', $max = 'now')
*
* @property string $time
*
* @method string time($format = 'H:i:s', $max = 'now')
*
* @property \DateTime $dateTimeBetween
*
* @method \DateTime dateTimeBetween($startDate = '-30 years', $endDate = 'now', $timezone = null)
*
* @property \DateTime $dateTimeInInterval
*
* @method \DateTime dateTimeInInterval($date = '-30 years', $interval = '+5 days', $timezone = null)
*
* @property \DateTime $dateTimeThisCentury
*
* @method \DateTime dateTimeThisCentury($max = 'now', $timezone = null)
*
* @property \DateTime $dateTimeThisDecade
*
* @method \DateTime dateTimeThisDecade($max = 'now', $timezone = null)
*
* @property \DateTime $dateTimeThisYear
*
* @method \DateTime dateTimeThisYear($max = 'now', $timezone = null)
*
* @property \DateTime $dateTimeThisMonth
*
* @method \DateTime dateTimeThisMonth($max = 'now', $timezone = null)
*
* @property string $amPm
*
* @method string amPm($max = 'now')
*
* @property string $dayOfMonth
*
* @method string dayOfMonth($max = 'now')
*
* @property string $dayOfWeek
*
* @method string dayOfWeek($max = 'now')
*
* @property string $month
*
* @method string month($max = 'now')
*
* @property string $monthName
*
* @method string monthName($max = 'now')
*
* @property string $year
*
* @method string year($max = 'now')
*
* @property string $century
*
* @method string century()
*
* @property string $timezone
*
* @method string timezone($countryCode = null)
*
* @property void $setDefaultTimezone
*
* @method void setDefaultTimezone($timezone = null)
*
* @property string $getDefaultTimezone
*
* @method string getDefaultTimezone()
*
* @property string $file
*
* @method string file($sourceDirectory = '/tmp', $targetDirectory = '/tmp', $fullPath = true)
*
* @property string $randomHtml
*
* @method string randomHtml($maxDepth = 4, $maxWidth = 4)
*
* @property string $imageUrl
*
* @method string imageUrl($width = 640, $height = 480, $category = null, $randomize = true, $word = null, $gray = false, string $format = 'png')
*
* @property string $image
*
* @method string image($dir = null, $width = 640, $height = 480, $category = null, $fullPath = true, $randomize = true, $word = null, $gray = false, string $format = 'png')
*
* @property string $email
*
* @method string email()
*
* @property string $safeEmail
*
* @method string safeEmail()
*
* @property string $freeEmail
*
* @method string freeEmail()
*
* @property string $companyEmail
*
* @method string companyEmail()
*
* @property string $freeEmailDomain
*
* @method string freeEmailDomain()
*
* @property string $safeEmailDomain
*
* @method string safeEmailDomain()
*
* @property string $userName
*
* @method string userName()
*
* @property string $password
*
* @method string password($minLength = 6, $maxLength = 20)
*
* @property string $domainName
*
* @method string domainName()
*
* @property string $domainWord
*
* @method string domainWord()
*
* @property string $tld
*
* @method string tld()
*
* @property string $url
*
* @method string url()
*
* @property string $slug
*
* @method string slug($nbWords = 6, $variableNbWords = true)
*
* @property string $ipv4
*
* @method string ipv4()
*
* @property string $ipv6
*
* @method string ipv6()
*
* @property string $localIpv4
*
* @method string localIpv4()
*
* @property string $macAddress
*
* @method string macAddress()
*
* @property string $word
*
* @method string word()
*
* @property array|string $words
*
* @method array|string words($nb = 3, $asText = false)
*
* @property string $sentence
*
* @method string sentence($nbWords = 6, $variableNbWords = true)
*
* @property array|string $sentences
*
* @method array|string sentences($nb = 3, $asText = false)
*
* @property string $paragraph
*
* @method string paragraph($nbSentences = 3, $variableNbSentences = true)
*
* @property array|string $paragraphs
*
* @method array|string paragraphs($nb = 3, $asText = false)
*
* @property string $text
*
* @method string text($maxNbChars = 200)
*
* @property bool $boolean
*
* @method bool boolean($chanceOfGettingTrue = 50)
*
* @property string $md5
*
* @method string md5()
*
* @property string $sha1
*
* @method string sha1()
*
* @property string $sha256
*
* @method string sha256()
*
* @property string $locale
*
* @method string locale()
*
* @property string $countryCode
*
* @method string countryCode()
*
* @property string $countryISOAlpha3
*
* @method string countryISOAlpha3()
*
* @property string $languageCode
*
* @method string languageCode()
*
* @property string $currencyCode
*
* @method string currencyCode()
*
* @property string $emoji
*
* @method string emoji()
*
* @property string $creditCardType
*
* @method string creditCardType()
*
* @property string $creditCardNumber
*
* @method string creditCardNumber($type = null, $formatted = false, $separator = '-')
*
* @property \DateTime $creditCardExpirationDate
*
* @method \DateTime creditCardExpirationDate($valid = true)
*
* @property string $creditCardExpirationDateString
*
* @method string creditCardExpirationDateString($valid = true, $expirationDateFormat = null)
*
* @property array $creditCardDetails
*
* @method array creditCardDetails($valid = true)
*
* @property string $iban
*
* @method string iban($countryCode = null, $prefix = '', $length = null)
*
* @property string $swiftBicNumber
*
* @method string swiftBicNumber()
*
* @property string $name
*
* @method string name($gender = null)
*
* @property string $firstName
*
* @method string firstName($gender = null)
*
* @property string $firstNameMale
*
* @method string firstNameMale()
*
* @property string $firstNameFemale
*
* @method string firstNameFemale()
*
* @property string $lastName
*
* @method string lastName($gender = null)
*
* @property string $title
*
* @method string title($gender = null)
*
* @property string $titleMale
*
* @method string titleMale()
*
* @property string $titleFemale
*
* @method string titleFemale()
*
* @property string $phoneNumber
*
* @method string phoneNumber()
*
* @property string $e164PhoneNumber
*
* @method string e164PhoneNumber()
*
* @property int $imei
*
* @method int imei()
*
* @property string $realText
*
* @method string realText($maxNbChars = 200, $indexSize = 2)
*
* @property string $realTextBetween
*
* @method string realTextBetween($minNbChars = 160, $maxNbChars = 200, $indexSize = 2)
*
* @property string $macProcessor
*
* @method string macProcessor()
*
* @property string $linuxProcessor
*
* @method string linuxProcessor()
*
* @property string $userAgent
*
* @method string userAgent()
*
* @property string $chrome
*
* @method string chrome()
*
* @property string $msedge
*
* @method string msedge()
*
* @property string $firefox
*
* @method string firefox()
*
* @property string $safari
*
* @method string safari()
*
* @property string $opera
*
* @method string opera()
*
* @property string $internetExplorer
*
* @method string internetExplorer()
*
* @property string $windowsPlatformToken
*
* @method string windowsPlatformToken()
*
* @property string $macPlatformToken
*
* @method string macPlatformToken()
*
* @property string $iosMobileToken
*
* @method string iosMobileToken()
*
* @property string $linuxPlatformToken
*
* @method string linuxPlatformToken()
*
* @property string $uuid
*
* @method string uuid()
*/
class Generator
{
protected $providers = [];
protected $formatters = [];
private $container;
/**
* @var UniqueGenerator
*/
private $uniqueGenerator;
public function __construct(?ContainerInterface $container = null)
{
$this->container = $container ?: Container\ContainerBuilder::withDefaultExtensions()->build();
}
/**
* @template T of Extension\Extension
*
* @param class-string<T> $id
*
* @throws Extension\ExtensionNotFound
*
* @return T
*/
public function ext(string $id): Extension\Extension
{
if (!$this->container->has($id)) {
throw new Extension\ExtensionNotFound(sprintf(
'No Faker extension with id "%s" was loaded.',
$id,
));
}
$extension = $this->container->get($id);
if ($extension instanceof Extension\GeneratorAwareExtension) {
$extension = $extension->withGenerator($this);
}
return $extension;
}
public function addProvider($provider)
{
array_unshift($this->providers, $provider);
$this->formatters = [];
}
public function getProviders()
{
return $this->providers;
}
/**
* With the unique generator you are guaranteed to never get the same two
* values.
*
* <code>
* // will never return twice the same value
* $faker->unique()->randomElement(array(1, 2, 3));
* </code>
*
* @param bool $reset If set to true, resets the list of existing values
* @param int $maxRetries Maximum number of retries to find a unique value,
* After which an OverflowException is thrown.
*
* @throws \OverflowException When no unique value can be found by iterating $maxRetries times
*
* @return self A proxy class returning only non-existing values
*/
public function unique($reset = false, $maxRetries = 10000)
{
if ($reset || $this->uniqueGenerator === null) {
$this->uniqueGenerator = new UniqueGenerator($this, $maxRetries);
}
return $this->uniqueGenerator;
}
/**
* Get a value only some percentage of the time.
*
* @param float $weight A probability between 0 and 1, 0 means that we always get the default value.
*
* @return self
*/
public function optional(float $weight = 0.5, $default = null)
{
if ($weight > 1) {
trigger_deprecation('fakerphp/faker', '1.16', 'First argument ($weight) to method "optional()" must be between 0 and 1. You passed %f, we assume you meant %f.', $weight, $weight / 100);
$weight = $weight / 100;
}
return new ChanceGenerator($this, $weight, $default);
}
/**
* To make sure the value meet some criteria, pass a callable that verifies the
* output. If the validator fails, the generator will try again.
*
* The value validity is determined by a function passed as first argument.
*
* <code>
* $values = array();
* $evenValidator = function ($digit) {
* return $digit % 2 === 0;
* };
* for ($i=0; $i < 10; $i++) {
* $values []= $faker->valid($evenValidator)->randomDigit;
* }
* print_r($values); // [0, 4, 8, 4, 2, 6, 0, 8, 8, 6]
* </code>
*
* @param ?\Closure $validator A function returning true for valid values
* @param int $maxRetries Maximum number of retries to find a valid value,
* After which an OverflowException is thrown.
*
* @throws \OverflowException When no valid value can be found by iterating $maxRetries times
*
* @return self A proxy class returning only valid values
*/
public function valid(?\Closure $validator = null, int $maxRetries = 10000)
{
return new ValidGenerator($this, $validator, $maxRetries);
}
public function seed($seed = null)
{
if ($seed === null) {
mt_srand();
} else {
mt_srand((int) $seed, self::mode());
}
}
/**
* @see https://www.php.net/manual/en/migration83.deprecated.php#migration83.deprecated.random
*/
private static function mode(): int
{
if (PHP_VERSION_ID < 80300) {
return MT_RAND_PHP;
}
return MT_RAND_MT19937;
}
public function format($format, $arguments = [])
{
return call_user_func_array($this->getFormatter($format), $arguments);
}
/**
* @param string $format
*
* @return callable
*/
public function getFormatter($format)
{
if (isset($this->formatters[$format])) {
return $this->formatters[$format];
}
if (method_exists($this, $format)) {
$this->formatters[$format] = [$this, $format];
return $this->formatters[$format];
}
// "Faker\Core\Barcode->ean13"
if (preg_match('|^([a-zA-Z0-9\\\]+)->([a-zA-Z0-9]+)$|', $format, $matches)) {
$this->formatters[$format] = [$this->ext($matches[1]), $matches[2]];
return $this->formatters[$format];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $format)) {
$this->formatters[$format] = [$provider, $format];
return $this->formatters[$format];
}
}
throw new \InvalidArgumentException(sprintf('Unknown format "%s"', $format));
}
/**
* Replaces tokens ('{{ tokenName }}') with the result from the token method call
*
* @param string $string String that needs to bet parsed
*
* @return string
*/
public function parse($string)
{
$callback = function ($matches) {
return $this->format($matches[1]);
};
return preg_replace_callback('/{{\s?(\w+|[\w\\\]+->\w+?)\s?}}/u', $callback, $string);
}
/**
* Get a random MIME type
*
* @example 'video/avi'
*/
public function mimeType()
{
return $this->ext(Extension\FileExtension::class)->mimeType();
}
/**
* Get a random file extension (without a dot)
*
* @example avi
*/
public function fileExtension()
{
return $this->ext(Extension\FileExtension::class)->extension();
}
/**
* Get a full path to a new real file on the system.
*/
public function filePath()
{
return $this->ext(Extension\FileExtension::class)->filePath();
}
/**
* Get an actual blood type
*
* @example 'AB'
*/
public function bloodType(): string
{
return $this->ext(Extension\BloodExtension::class)->bloodType();
}
/**
* Get a random resis value
*
* @example '+'
*/
public function bloodRh(): string
{
return $this->ext(Extension\BloodExtension::class)->bloodRh();
}
/**
* Get a full blood group
*
* @example 'AB+'
*/
public function bloodGroup(): string
{
return $this->ext(Extension\BloodExtension::class)->bloodGroup();
}
/**
* Get a random EAN13 barcode.
*
* @example '4006381333931'
*/
public function ean13(): string
{
return $this->ext(Extension\BarcodeExtension::class)->ean13();
}
/**
* Get a random EAN8 barcode.
*
* @example '73513537'
*/
public function ean8(): string
{
return $this->ext(Extension\BarcodeExtension::class)->ean8();
}
/**
* Get a random ISBN-10 code
*
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number
*
* @example '4881416324'
*/
public function isbn10(): string
{
return $this->ext(Extension\BarcodeExtension::class)->isbn10();
}
/**
* Get a random ISBN-13 code
*
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number
*
* @example '9790404436093'
*/
public function isbn13(): string
{
return $this->ext(Extension\BarcodeExtension::class)->isbn13();
}
/**
* Returns a random number between $int1 and $int2 (any order)
*
* @example 79907610
*/
public function numberBetween($int1 = 0, $int2 = 2147483647): int
{
return $this->ext(Extension\NumberExtension::class)->numberBetween((int) $int1, (int) $int2);
}
/**
* Returns a random number between 0 and 9
*/
public function randomDigit(): int
{
return $this->ext(Extension\NumberExtension::class)->randomDigit();
}
/**
* Generates a random digit, which cannot be $except
*/
public function randomDigitNot($except): int
{
return $this->ext(Extension\NumberExtension::class)->randomDigitNot((int) $except);
}
/**
* Returns a random number between 1 and 9
*/
public function randomDigitNotZero(): int
{
return $this->ext(Extension\NumberExtension::class)->randomDigitNotZero();
}
/**
* Return a random float number
*
* @example 48.8932
*/
public function randomFloat($nbMaxDecimals = null, $min = 0, $max = null): float
{
return $this->ext(Extension\NumberExtension::class)->randomFloat(
$nbMaxDecimals !== null ? (int) $nbMaxDecimals : null,
(float) $min,
$max !== null ? (float) $max : null,
);
}
/**
* Returns a random integer with 0 to $nbDigits digits.
*
* The maximum value returned is mt_getrandmax()
*
* @param int|null $nbDigits Defaults to a random number between 1 and 9
* @param bool $strict Whether the returned number should have exactly $nbDigits
*
* @example 79907610
*/
public function randomNumber($nbDigits = null, $strict = false): int
{
return $this->ext(Extension\NumberExtension::class)->randomNumber(
$nbDigits !== null ? (int) $nbDigits : null,
(bool) $strict,
);
}
/**
* Get a version number in semantic versioning syntax 2.0.0. (https://semver.org/spec/v2.0.0.html)
*
* @param bool $preRelease Pre release parts may be randomly included
* @param bool $build Build parts may be randomly included
*
* @example 1.0.0
* @example 1.0.0-alpha.1
* @example 1.0.0-alpha.1+b71f04d
*/
public function semver(bool $preRelease = false, bool $build = false): string
{
return $this->ext(Extension\VersionExtension::class)->semver($preRelease, $build);
}
/**
* @deprecated
*/
protected function callFormatWithMatches($matches)
{
trigger_deprecation('fakerphp/faker', '1.14', 'Protected method "callFormatWithMatches()" is deprecated and will be removed.');
return $this->format($matches[1]);
}
/**
* @param string $attribute
*
* @deprecated Use a method instead.
*/
public function __get($attribute)
{
trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);
return $this->format($attribute);
}
/**
* @param string $method
* @param array $attributes
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
public function __destruct()
{
$this->seed();
}
public function __wakeup()
{
$this->formatters = [];
}
}
@@ -0,0 +1,180 @@
<?php
namespace Faker\Guesser;
use Faker\Provider\Base;
class Name
{
protected $generator;
public function __construct(\Faker\Generator $generator)
{
$this->generator = $generator;
}
/**
* @param string $name
* @param int|null $size Length of field, if known
*
* @return callable|null
*/
public function guessFormat($name, $size = null)
{
$name = Base::toLower($name);
$generator = $this->generator;
if (preg_match('/^is[_A-Z]/', $name)) {
return static function () use ($generator) {
return $generator->boolean();
};
}
if (preg_match('/(_a|A)t$/', $name)) {
return static function () use ($generator) {
return $generator->dateTime();
};
}
switch (str_replace('_', '', $name)) {
case 'firstname':
return static function () use ($generator) {
return $generator->firstName();
};
case 'lastname':
return static function () use ($generator) {
return $generator->lastName();
};
case 'username':
case 'login':
return static function () use ($generator) {
return $generator->userName();
};
case 'email':
case 'emailaddress':
return static function () use ($generator) {
return $generator->email();
};
case 'phonenumber':
case 'phone':
case 'telephone':
case 'telnumber':
return static function () use ($generator) {
return $generator->phoneNumber();
};
case 'address':
return static function () use ($generator) {
return $generator->address();
};
case 'city':
case 'town':
return static function () use ($generator) {
return $generator->city();
};
case 'streetaddress':
return static function () use ($generator) {
return $generator->streetAddress();
};
case 'postcode':
case 'zipcode':
return static function () use ($generator) {
return $generator->postcode();
};
case 'state':
return static function () use ($generator) {
return $generator->state();
};
case 'county':
if ($this->generator->locale == 'en_US') {
return static function () use ($generator) {
return sprintf('%s County', $generator->city());
};
}
return static function () use ($generator) {
return $generator->state();
};
case 'country':
switch ($size) {
case 2:
return static function () use ($generator) {
return $generator->countryCode();
};
case 3:
return static function () use ($generator) {
return $generator->countryISOAlpha3();
};
case 5:
case 6:
return static function () use ($generator) {
return $generator->locale();
};
default:
return static function () use ($generator) {
return $generator->country();
};
}
break;
case 'locale':
return static function () use ($generator) {
return $generator->locale();
};
case 'currency':
case 'currencycode':
return static function () use ($generator) {
return $generator->currencyCode();
};
case 'url':
case 'website':
return static function () use ($generator) {
return $generator->url();
};
case 'company':
case 'companyname':
case 'employer':
return static function () use ($generator) {
return $generator->company();
};
case 'title':
if ($size !== null && $size <= 10) {
return static function () use ($generator) {
return $generator->title();
};
}
return static function () use ($generator) {
return $generator->sentence();
};
case 'body':
case 'summary':
case 'article':
case 'description':
return static function () use ($generator) {
return $generator->text();
};
}
return null;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Faker\ORM\Doctrine;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Faker\Generator;
require_once 'backward-compatibility.php';
class ColumnTypeGuesser
{
protected $generator;
public function __construct(Generator $generator)
{
$this->generator = $generator;
}
/**
* @return \Closure|null
*/
public function guessFormat($fieldName, ClassMetadata $class)
{
$generator = $this->generator;
$type = $class->getTypeOfField($fieldName);
switch ($type) {
case 'boolean':
return static function () use ($generator) {
return $generator->boolean();
};
case 'decimal':
$size = $class->fieldMappings[$fieldName]['precision'] ?? 2;
return static function () use ($generator, $size) {
return $generator->randomNumber($size + 2) / 100;
};
case 'smallint':
return static function () use ($generator) {
return $generator->numberBetween(0, 65535);
};
case 'integer':
return static function () use ($generator) {
return $generator->numberBetween(0, 2147483647);
};
case 'bigint':
return static function () use ($generator) {
return $generator->numberBetween(0, PHP_INT_MAX);
};
case 'float':
return static function () use ($generator) {
return $generator->randomFloat();
};
case 'string':
$size = $class->fieldMappings[$fieldName]['length'] ?? 255;
return static function () use ($generator, $size) {
return $generator->text($size);
};
case 'text':
return static function () use ($generator) {
return $generator->text();
};
case 'datetime':
case 'date':
case 'time':
return static function () use ($generator) {
return $generator->datetime();
};
case 'datetime_immutable':
case 'date_immutable':
case 'time_immutable':
return static function () use ($generator) {
return \DateTimeImmutable::createFromMutable($generator->datetime);
};
default:
// no smart way to guess what the user expects here
return null;
}
}
}
@@ -0,0 +1,126 @@
<?php
namespace Faker\ORM\Doctrine;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Generator;
require_once 'backward-compatibility.php';
/**
* Service class for populating a database using the Doctrine ORM or ODM.
* A Populator can populate several tables using ActiveRecord classes.
*/
class Populator
{
/**
* @var int
*/
protected $batchSize;
/**
* @var Generator
*/
protected $generator;
/**
* @var ObjectManager|null
*/
protected $manager;
/**
* @var array
*/
protected $entities = [];
/**
* @var array
*/
protected $quantities = [];
/**
* @var array
*/
protected $generateId = [];
/**
* Populator constructor.
*
* @param int $batchSize
*/
public function __construct(Generator $generator, ?ObjectManager $manager = null, $batchSize = 1000)
{
$this->generator = $generator;
$this->manager = $manager;
$this->batchSize = $batchSize;
}
/**
* Add an order for the generation of $number records for $entity.
*
* @param mixed $entity A Doctrine classname, or a \Faker\ORM\Doctrine\EntityPopulator instance
* @param int $number The number of entities to populate
*/
public function addEntity($entity, $number, $customColumnFormatters = [], $customModifiers = [], $generateId = false)
{
if (!$entity instanceof \Faker\ORM\Doctrine\EntityPopulator) {
if (null === $this->manager) {
throw new \InvalidArgumentException('No entity manager passed to Doctrine Populator.');
}
$entity = new \Faker\ORM\Doctrine\EntityPopulator($this->manager->getClassMetadata($entity));
}
$entity->setColumnFormatters($entity->guessColumnFormatters($this->generator));
if ($customColumnFormatters) {
$entity->mergeColumnFormattersWith($customColumnFormatters);
}
$entity->mergeModifiersWith($customModifiers);
$this->generateId[$entity->getClass()] = $generateId;
$class = $entity->getClass();
$this->entities[$class] = $entity;
$this->quantities[$class] = $number;
}
/**
* Populate the database using all the Entity classes previously added.
*
* Please note that large amounts of data will result in more memory usage since the the Populator will return
* all newly created primary keys after executing.
*
* @param ObjectManager|null $entityManager A Doctrine connection object
*
* @return array A list of the inserted PKs
*/
public function execute($entityManager = null)
{
if (null === $entityManager) {
$entityManager = $this->manager;
}
if (null === $entityManager) {
throw new \InvalidArgumentException('No entity manager passed to Doctrine Populator.');
}
$insertedEntities = [];
foreach ($this->quantities as $class => $number) {
$generateId = $this->generateId[$class];
for ($i = 0; $i < $number; ++$i) {
$insertedEntities[$class][] = $this->entities[$class]->execute(
$entityManager,
$insertedEntities,
$generateId,
);
if (count($insertedEntities) % $this->batchSize === 0) {
$entityManager->flush();
}
}
$entityManager->flush();
}
return $insertedEntities;
}
}
@@ -0,0 +1,89 @@
<?php
namespace Faker\ORM\Spot;
use Spot\Locator;
/**
* Service class for populating a database using the Spot ORM.
*/
class Populator
{
protected $generator;
protected $locator;
protected $entities = [];
protected $quantities = [];
/**
* Populator constructor.
*/
public function __construct(\Faker\Generator $generator, ?Locator $locator = null)
{
$this->generator = $generator;
$this->locator = $locator;
}
/**
* Add an order for the generation of $number records for $entity.
*
* @param string $entityName Name of Entity object to generate
* @param int $number The number of entities to populate
* @param array $customColumnFormatters
* @param array $customModifiers
* @param bool $useExistingData Should we use existing rows (e.g. roles) to populate relations?
*/
public function addEntity(
$entityName,
$number,
$customColumnFormatters = [],
$customModifiers = [],
$useExistingData = false
) {
$mapper = $this->locator->mapper($entityName);
if (null === $mapper) {
throw new \InvalidArgumentException('No mapper can be found for entity ' . $entityName);
}
$entity = new EntityPopulator($mapper, $this->locator, $useExistingData);
$entity->setColumnFormatters($entity->guessColumnFormatters($this->generator));
if ($customColumnFormatters) {
$entity->mergeColumnFormattersWith($customColumnFormatters);
}
$entity->mergeModifiersWith($customModifiers);
$this->entities[$entityName] = $entity;
$this->quantities[$entityName] = $number;
}
/**
* Populate the database using all the Entity classes previously added.
*
* @param Locator $locator A Spot locator
*
* @return array A list of the inserted PKs
*/
public function execute($locator = null)
{
if (null === $locator) {
$locator = $this->locator;
}
if (null === $locator) {
throw new \InvalidArgumentException('No entity manager passed to Spot Populator.');
}
$insertedEntities = [];
foreach ($this->quantities as $entityName => $number) {
for ($i = 0; $i < $number; ++$i) {
$insertedEntities[$entityName][] = $this->entities[$entityName]->execute(
$insertedEntities,
);
}
}
return $insertedEntities;
}
}
@@ -0,0 +1,389 @@
<?php
namespace Faker\Provider;
class DateTime extends Base
{
protected static $century = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI'];
protected static $defaultTimezone = null;
/**
* @param \DateTime|float|int|string $max
*
* @return false|int
*/
protected static function getMaxTimestamp($max = 'now')
{
if (is_numeric($max)) {
return (int) $max;
}
if ($max instanceof \DateTime) {
return $max->getTimestamp();
}
return strtotime(empty($max) ? 'now' : $max);
}
/**
* Get a timestamp between January 1, 1970, and now
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return int
*
* @example 1061306726
*/
public static function unixTime($max = 'now')
{
return self::numberBetween(0, static::getMaxTimestamp($max));
}
/**
* Get a datetime object for a date between January 1, 1970 and now
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*
* @example DateTime('2005-08-16 20:39:21')
*/
public static function dateTime($max = 'now', $timezone = null)
{
return static::setTimezone(
new \DateTime('@' . static::unixTime($max)),
$timezone,
);
}
/**
* Get a datetime object for a date between January 1, 001 and now
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*
* @example DateTime('1265-03-22 21:15:52')
*/
public static function dateTimeAD($max = 'now', $timezone = null)
{
$min = (PHP_INT_SIZE > 4 ? -62135597361 : -PHP_INT_MAX);
return static::setTimezone(
new \DateTime('@' . self::numberBetween($min, static::getMaxTimestamp($max))),
$timezone,
);
}
/**
* get a date string formatted with ISO8601
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '2003-10-21T16:05:52+0000'
*/
public static function iso8601($max = 'now')
{
return static::date(\DateTime::ISO8601, $max);
}
/**
* Get a date string between January 1, 1970 and now
*
* @param string $format
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '2008-11-27'
*/
public static function date($format = 'Y-m-d', $max = 'now')
{
return static::dateTime($max)->format($format);
}
/**
* Get a time string (24h format by default)
*
* @param string $format
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '15:02:34'
*/
public static function time($format = 'H:i:s', $max = 'now')
{
return static::dateTime($max)->format($format);
}
/**
* Get a DateTime object based on a random date between two given dates.
* Accepts date strings that can be recognized by strtotime().
*
* @param \DateTime|string $startDate Defaults to 30 years ago
* @param \DateTime|string $endDate Defaults to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*
* @example DateTime('1999-02-02 11:42:52')
*/
public static function dateTimeBetween($startDate = '-30 years', $endDate = 'now', $timezone = null)
{
$startTimestamp = $startDate instanceof \DateTime ? $startDate->getTimestamp() : strtotime($startDate);
$endTimestamp = static::getMaxTimestamp($endDate);
if ($startTimestamp > $endTimestamp) {
throw new \InvalidArgumentException('Start date must be anterior to end date.');
}
$timestamp = self::numberBetween($startTimestamp, $endTimestamp);
return static::setTimezone(
new \DateTime('@' . $timestamp),
$timezone,
);
}
/**
* Get a DateTime object based on a random date between one given date and
* an interval
* Accepts date string that can be recognized by strtotime().
*
* @param \DateTime|string $date Defaults to 30 years ago
* @param string $interval Defaults to 5 days after
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*
* @example dateTimeInInterval('1999-02-02 11:42:52', '+ 5 days')
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public static function dateTimeInInterval($date = '-30 years', $interval = '+5 days', $timezone = null)
{
$intervalObject = \DateInterval::createFromDateString($interval);
$datetime = $date instanceof \DateTime ? $date : new \DateTime($date);
$otherDatetime = clone $datetime;
$otherDatetime->add($intervalObject);
$begin = min($datetime, $otherDatetime);
$end = $datetime === $begin ? $otherDatetime : $datetime;
return static::dateTimeBetween(
$begin,
$end,
$timezone,
);
}
/**
* Get a date time object somewhere within a century.
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*/
public static function dateTimeThisCentury($max = 'now', $timezone = null)
{
return static::dateTimeBetween('-100 year', $max, $timezone);
}
/**
* Get a date time object somewhere within a decade.
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*/
public static function dateTimeThisDecade($max = 'now', $timezone = null)
{
return static::dateTimeBetween('-10 year', $max, $timezone);
}
/**
* Get a date time object somewhere inside the current year.
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*/
public static function dateTimeThisYear($max = 'now', $timezone = null)
{
return static::dateTimeBetween('first day of january this year', $max, $timezone);
}
/**
* Get a date time object somewhere within a month.
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
* @param string|null $timezone time zone in which the date time should be set, default to DateTime::$defaultTimezone, if set, otherwise the result of `date_default_timezone_get`
*
* @return \DateTime
*/
public static function dateTimeThisMonth($max = 'now', $timezone = null)
{
return static::dateTimeBetween('-1 month', $max, $timezone);
}
/**
* Get a string containing either "am" or "pm".
*
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example 'am'
*/
public static function amPm($max = 'now')
{
return static::dateTime($max)->format('a');
}
/**
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '22'
*/
public static function dayOfMonth($max = 'now')
{
return static::dateTime($max)->format('d');
}
/**
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example 'Tuesday'
*/
public static function dayOfWeek($max = 'now')
{
return static::dateTime($max)->format('l');
}
/**
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '7'
*/
public static function month($max = 'now')
{
return static::dateTime($max)->format('m');
}
/**
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example 'September'
*/
public static function monthName($max = 'now')
{
return static::dateTime($max)->format('F');
}
/**
* @param \DateTime|int|string $max maximum timestamp used as random end limit, default to "now"
*
* @return string
*
* @example '1987'
*/
public static function year($max = 'now')
{
return static::dateTime($max)->format('Y');
}
/**
* @return string
*
* @example 'XVII'
*/
public static function century()
{
return static::randomElement(static::$century);
}
/**
* @return string
*
* @example 'Europe/Paris'
*/
public static function timezone(?string $countryCode = null)
{
if ($countryCode) {
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $countryCode);
} else {
$timezones = \DateTimeZone::listIdentifiers();
}
return static::randomElement($timezones);
}
/**
* Internal method to set the time zone on a DateTime.
*
* @param string|null $timezone
*
* @return \DateTime
*/
private static function setTimezone(\DateTime $dt, $timezone)
{
return $dt->setTimezone(new \DateTimeZone(static::resolveTimezone($timezone)));
}
/**
* Sets default time zone.
*
* @param string $timezone
*/
public static function setDefaultTimezone($timezone = null)
{
static::$defaultTimezone = $timezone;
}
/**
* Gets default time zone.
*
* @return string|null
*/
public static function getDefaultTimezone()
{
return static::$defaultTimezone;
}
/**
* @param string|null $timezone
*
* @return string|null
*/
private static function resolveTimezone($timezone)
{
return (null === $timezone) ? ((null === static::$defaultTimezone) ? date_default_timezone_get() : static::$defaultTimezone) : $timezone;
}
}

Some files were not shown because too many files have changed in this diff Show More