mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-13 11:41:31 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Africa's Talking PHP SDK
|
||||
|
||||
[](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).
|
||||
+32
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
+47
@@ -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());
|
||||
}
|
||||
}
|
||||
+64
@@ -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']);
|
||||
}
|
||||
}
|
||||
+25
@@ -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']);
|
||||
}
|
||||
}
|
||||
+67
@@ -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']);
|
||||
}
|
||||
}
|
||||
+138
@@ -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']);
|
||||
}
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
+91
@@ -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
@@ -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' => [],
|
||||
],
|
||||
|
||||
];
|
||||
+108
@@ -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
@@ -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
@@ -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" "$@"
|
||||
@@ -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.
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
+572
@@ -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',
|
||||
);
|
||||
@@ -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'),
|
||||
);
|
||||
@@ -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
+9801
File diff suppressed because it is too large
Load Diff
+1482
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
|
||||
Vendored
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
+120
@@ -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'];
|
||||
}
|
||||
}
|
||||
+231
@@ -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();
|
||||
}
|
||||
}
|
||||
+93
@@ -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();
|
||||
}
|
||||
}
|
||||
Vendored
+38
@@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
+1497
File diff suppressed because it is too large
Load Diff
+4727
File diff suppressed because it is too large
Load Diff
+13
@@ -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
|
||||
{
|
||||
}
|
||||
+131
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
@@ -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)];
|
||||
}
|
||||
}
|
||||
+16
@@ -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();
|
||||
}
|
||||
}
|
||||
+1329
File diff suppressed because it is too large
Load Diff
+1420
File diff suppressed because it is too large
Load Diff
+1841
File diff suppressed because it is too large
Load Diff
+1545
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));
|
||||
}
|
||||
}
|
||||
+1808
File diff suppressed because it is too large
Load Diff
+605
@@ -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;
|
||||
}
|
||||
}
|
||||
+537
@@ -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);
|
||||
}
|
||||
}
|
||||
+769
@@ -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;
|
||||
}
|
||||
}
|
||||
+788
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+219
@@ -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
|
||||
==========================
|
||||
|
||||
[](https://packagist.org/packages/dragonmantank/cron-expression) [](https://packagist.org/packages/dragonmantank/cron-expression) [](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml) [](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
|
||||
}
|
||||
}
|
||||
}
|
||||
+591
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
Vendored
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+327
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+27
@@ -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";
|
||||
}
|
||||
}
|
||||
+17
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
+242
@@ -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;
|
||||
}
|
||||
}
|
||||
+91
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -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
Reference in New Issue
Block a user